Compare commits
16 Commits
8007eeb043
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| f73c45bb48 | |||
| dc270e6785 | |||
| 456577f2f7 | |||
| 027f601887 | |||
| d2f855881c | |||
| 454a259e37 | |||
| c82957cc61 | |||
| ede1f92981 | |||
| 90f33d455f | |||
| ecb1496a54 | |||
| 39709662af | |||
| 29fdf6a2f3 | |||
| 44c87e1e50 | |||
| 8851f710fb | |||
| 28b7138547 | |||
| 02b10131f9 |
59
.gitea/workflows/work.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Build and Deploy on Tag
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
PACKAGE_AND_BREW: ${{ secrets.PACKAGE_AND_BREW }}
|
||||||
|
VERSION: ${{ gitea.ref_name }}
|
||||||
|
GITEA_DOMAIN: git.tesses.org
|
||||||
|
GITEA_REGISTRY_USER: tesses50
|
||||||
|
RESULT_IMAGE_NAME: tesses50/tytd2025
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-packages:
|
||||||
|
name: "🔨 Build"
|
||||||
|
runs-on: fw13-ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: "git.tesses.org/tesses50/linux-x64:latest"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Log in to registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.GITEA_DOMAIN }}
|
||||||
|
username: ${{ env.GITEA_REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.PACKAGE_AND_BREW }}
|
||||||
|
- name: Build and push image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.GITEA_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}:latest
|
||||||
|
- name: Install CrossLang
|
||||||
|
uses: https://git.tesses.org/tesses50/setup-crosslang@v1
|
||||||
|
- run: |
|
||||||
|
bash build-for-system-pkg.sh
|
||||||
|
cd Tesses.YouTubeDownloader
|
||||||
|
crosslang upload-package --token="${{ secrets.CPKG_KEY }}" --host="https://cpkg.tesseslanguage.com/"
|
||||||
|
cd ../Tesses.YouTubeDownloader.Server
|
||||||
|
crosslang upload-package --token="${{ secrets.CPKG_KEY }}" --host="https://cpkg.tesseslanguage.com/"
|
||||||
|
cd ../Tesses.YouTubeDownloader.PluginTemplate
|
||||||
|
crosslang upload-package --token="${{ secrets.CPKG_KEY }}" --host="https://cpkg.tesseslanguage.com/"
|
||||||
|
|
||||||
|
- uses: akkuman/gitea-release-action@v1
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
files: |-
|
||||||
|
artifacts/**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
6
.gitignore
vendored
@@ -1,4 +1,8 @@
|
|||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
publish
|
publish
|
||||||
TYTD
|
TYTD
|
||||||
|
artifacts
|
||||||
|
*.pkg.tar.zst
|
||||||
|
*.deb
|
||||||
|
*.rpm
|
||||||
13
Dockerfile
@@ -1,9 +1,14 @@
|
|||||||
FROM onedev.site.tesses.net/crosslang/crosslang:latest
|
FROM git.tesses.org/tesses50/crosslang-withshell:latest AS build
|
||||||
RUN mkdir /app
|
|
||||||
COPY Tesses.YouTubeDownloader.Server/bin/ /app
|
COPY ./ /src/
|
||||||
|
|
||||||
|
RUN cd /src/Tesses.YouTubeDownloader.Server && crosslang build && cd bin && mv Tesses.YouTubeDownloader.Server-*.crvm tytd2025.crvm
|
||||||
|
|
||||||
|
FROM git.tesses.org/tesses50/crosslang:latest
|
||||||
|
COPY --from=build /src/Tesses.YouTubeDownloader.Server/bin/ /app
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
ENV TYTDDIR=/data
|
ENV TYTDDIR=/data
|
||||||
|
|
||||||
EXPOSE 3255
|
EXPOSE 3255
|
||||||
|
|
||||||
ENTRYPOINT ["crossvm","/app/Tesses.YouTubeDownloader.Server-1.0.0.0-prod.crvm","--port=3255"]
|
ENTRYPOINT ["crosslang","vm","/app/tytd2025.crvm","--port=3255"]
|
||||||
|
|||||||
66
README.md
@@ -1,39 +1,71 @@
|
|||||||
# TYTD 2025
|
# TYTD 2025
|
||||||
A YouTube downloader writen in [CrossLang](https://crosslang.tesseslanguage.com/)
|
My web based YouTube downloader that I created in 2025 written in [CrossLang](https://crosslang.tesseslanguage.com/), my own language
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[Website](https://crosslang.tesseslanguage.com/software/webapps/tytd2025/)
|
||||||
|
|
||||||
|
# Features
|
||||||
|
- PWA with the ability to add videos when server is down
|
||||||
|
- Can download videos, playlists and channels (you need a channel url like this [https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A](https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A))
|
||||||
|
- Can subscribe to channels
|
||||||
|
- Can create playlists (that are stored on the server)
|
||||||
|
- Search and browse your downloaded videos, playlists, or channels (the search is very basic though)
|
||||||
|
- User accounts
|
||||||
|
- Videos can be tagged based on your downloader's TYTD tag (to determine which instance downloaded it)
|
||||||
|
- Plugins from [CPKG](https://cpkg.tesseslanguage.com/) or any CPKG compliant server
|
||||||
|
- Can download YouTube videos either Low quality (but doesn't) require [ffmpeg](https://ffmpeg.org/), MP3, FLAC or AMV (these do need [ffmpeg](https://ffmpeg.org/) in your PATH however)
|
||||||
|
- Runs on the Wii using the [Wii Linux Continuation Project](https://wiibrew.org/wiki/Wii-Linux#Wii_Linux_Continuation_Project) (albeit extremely slowly, despite this that's where I run it)
|
||||||
|
|
||||||
|
# What this project uses (attribution)
|
||||||
|
I don't feel like storing their licenses in my project, so I link to their projects instead
|
||||||
|
|
||||||
|
- [SQLite3](https://www.sqlite.org/) for it's database (embedded into TessesFramework)
|
||||||
|
- [BeerCSS](https://www.beercss.com/) for its webui, licensed under MIT
|
||||||
|
- [HTMX](https://htmx.org/) for the SPA experience
|
||||||
|
- [FFmpeg](https://ffmpeg.org/) if you convert the videos, uses the cli
|
||||||
|
- [YouTubeExplode](https://github.com/Tyrrrz/YoutubeExplode) for some of its json payloads
|
||||||
|
- [NewPipe](https://newpipe.net/) for some of its json payloads
|
||||||
|
- [Material Symbols Font](https://fonts.google.com/icons) (wget from beercss css files)
|
||||||
|
- [VideoJS](https://videojs.org/) for video player
|
||||||
|
- [CrossLang](https://crosslang.tesseslanguage.com/) My programming language
|
||||||
|
|
||||||
|
|
||||||
## To Install
|
## To Install
|
||||||
Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
|
Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
|
||||||
|
|
||||||
from source:
|
from source:
|
||||||
```bash
|
```bash
|
||||||
cd Tesses.YouTubeDownloader.Server
|
git clone https://git.tesses.org/tesses50/tytd2025
|
||||||
|
cd tytd2025/Tesses.YouTubeDownloader.Server
|
||||||
crosslang install-webapp
|
crosslang install-webapp
|
||||||
|
|
||||||
# Save to Videos
|
|
||||||
crosslang webapp tytd2025 --port=3255
|
|
||||||
|
|
||||||
# Save anywhere (unix, replace /path/to/tytd with the folder you want)
|
|
||||||
export TYTDDIR=/path/to/tytd
|
|
||||||
crosslang webapp tytd2025 --port=3255
|
|
||||||
|
|
||||||
# Save anywhere (windows, replace C:\path\to\tytd with the folder you want)
|
|
||||||
SET TYTDDIR=/path/to/tytd
|
|
||||||
crosslang webapp tytd2025 --port=3255
|
|
||||||
```
|
```
|
||||||
|
|
||||||
from package manager:
|
from package manager:
|
||||||
```bash
|
```bash
|
||||||
crosslang install-webapp Tesses.YouTubeDownloader.Server
|
crosslang install-webapp Tesses.YouTubeDownloader.Server
|
||||||
|
```
|
||||||
|
|
||||||
# Save to Videos
|
|
||||||
|
## To Run
|
||||||
|
|
||||||
|
Save to videos:
|
||||||
|
```bash
|
||||||
crosslang webapp tytd2025 --port=3255
|
crosslang webapp tytd2025 --port=3255
|
||||||
|
```
|
||||||
|
|
||||||
# Save anywhere (unix, replace /path/to/tytd with the folder you want)
|
Save to a folder (unix):
|
||||||
|
```bash
|
||||||
|
# Replace /path/to/tytd with the folder you want
|
||||||
export TYTDDIR=/path/to/tytd
|
export TYTDDIR=/path/to/tytd
|
||||||
crosslang webapp tytd2025 --port=3255
|
crosslang webapp tytd2025 --port=3255
|
||||||
|
```
|
||||||
|
|
||||||
# Save anywhere (windows, replace C:\path\to\tytd with the folder you want)
|
Save to a folder (windows):
|
||||||
SET TYTDDIR=/path/to/tytd
|
```batch
|
||||||
|
REM Replace C:\path\to\tytd with the folder you want
|
||||||
|
SET TYTDDIR=C:\path\to\tytd
|
||||||
crosslang webapp tytd2025 --port=3255
|
crosslang webapp tytd2025 --port=3255
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,6 @@
|
|||||||
"template_icon": "icon.png"
|
"template_icon": "icon.png"
|
||||||
},
|
},
|
||||||
"name": "Tesses.YouTubeDownloader.PluginTemplate",
|
"name": "Tesses.YouTubeDownloader.PluginTemplate",
|
||||||
"version": "1.0.0.0-prod",
|
"version": "1.0.1.0-dev",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"icon": "icon.png",
|
"icon": "tytd-128.png",
|
||||||
"info": {
|
"info": {
|
||||||
"description": "Download YouTube Videos (using CrossLang, via web interface, great for homelabs)",
|
"description": "Download YouTube Videos (using CrossLang, via web interface, great for homelabs)",
|
||||||
"maintainer": "Mike Nolan",
|
"maintainer": "Mike Nolan",
|
||||||
@@ -14,5 +14,6 @@
|
|||||||
"project_dependencies": [
|
"project_dependencies": [
|
||||||
"..\/Tesses.YouTubeDownloader"
|
"..\/Tesses.YouTubeDownloader"
|
||||||
],
|
],
|
||||||
"version": "1.0.0.0-dev"
|
"version": "1.0.1.0-dev",
|
||||||
|
"compTime": "secure"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div>
|
||||||
|
<h1>Cannot connect to the TYTD2025 server.</h1>
|
||||||
|
Refresh page to add to offline queue
|
||||||
|
</div>
|
||||||
110
Tesses.YouTubeDownloader.Server/res/offline.html
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TYTD2025 is Offline</title>
|
||||||
|
<link rel="stylesheet" href="/beer.min.css">
|
||||||
|
<link rel="stylesheet" href="/theme.css">
|
||||||
|
<script src="/offline.js" defer></script>
|
||||||
|
<script type="module" src="/beer.min.js" defer></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<a href="./" class="button extend circle">
|
||||||
|
<i>home</i>
|
||||||
|
<span>Home</span>
|
||||||
|
</a>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max"></div>
|
||||||
|
<div class="min">
|
||||||
|
<img src="tytd-128.png" alt="tytd-logo">
|
||||||
|
</div>
|
||||||
|
<div class="max"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max"></div>
|
||||||
|
<div class="min">
|
||||||
|
<h3>Cannot connect to the TYTD2025 server.</h3>
|
||||||
|
</div>
|
||||||
|
<div class="max"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max"></div>
|
||||||
|
<div class="min">
|
||||||
|
<form>
|
||||||
|
|
||||||
|
<div class="field label border small fill">
|
||||||
|
<input id="url" type="text">
|
||||||
|
<label>Url or id</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="max">
|
||||||
|
<div class="field label suffix border fill">
|
||||||
|
<select id="res">
|
||||||
|
|
||||||
|
|
||||||
|
<option value="NoDownload">Don't Download</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="LowVideo" selected="">Low (muxed by YouTube)</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="VideoOnly">Highest video (no audio)</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="AudioOnly">Highest audio (no video)</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="MP3">Convert to MP3</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="FLAC">Convert to FLAC</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="MP4">Convert to MP4</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="MKV">Mux to MKV (no transcoding)</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="DontConvert">Don't convert or Mux</option>
|
||||||
|
|
||||||
|
|
||||||
|
</select>
|
||||||
|
<label>Resolution</label>
|
||||||
|
<i>arrow_drop_down</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min">
|
||||||
|
<button id="btn">
|
||||||
|
<i>add</i>
|
||||||
|
Add when online
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="max"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
btn.onclick = (evt)=>{
|
||||||
|
evt.preventDefault();
|
||||||
|
addOffline(url.value, res.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
54
Tesses.YouTubeDownloader.Server/res/offline.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
function addOffline(url,res)
|
||||||
|
{
|
||||||
|
var videos = JSON.parse(localStorage.getItem("videos") ?? "[]");
|
||||||
|
videos.push({
|
||||||
|
url: url,
|
||||||
|
res: res
|
||||||
|
});
|
||||||
|
localStorage.setItem('videos',JSON.stringify(videos));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncOffline()
|
||||||
|
{
|
||||||
|
const json = localStorage.getItem("videos") ?? "[]";
|
||||||
|
if(json !== "[]")
|
||||||
|
{
|
||||||
|
const resp = await fetch('/api/v1/add',{
|
||||||
|
body: json,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
if(resp.ok)
|
||||||
|
{
|
||||||
|
localStorage.removeItem('videos');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(navigator.online)
|
||||||
|
{
|
||||||
|
syncOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('online',()=>{
|
||||||
|
syncOffline();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getPersonalTempLink()
|
||||||
|
{
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const ent = searchParams.get("name");
|
||||||
|
if(ent)
|
||||||
|
{
|
||||||
|
const resp=await fetch(`./api/v1/personal_tmp_link?name=${encodeURIComponent(ent)}`);
|
||||||
|
if(resp.ok)
|
||||||
|
{
|
||||||
|
const text = await resp.text();
|
||||||
|
the_playlist_url.innerText = text;
|
||||||
|
the_playlist_url.href = text;
|
||||||
|
ui("#dialog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Tesses.YouTubeDownloader.Server/res/service_worker.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const assets = ["<@ASSETS@>"];
|
||||||
|
|
||||||
|
const staticCacheName = "tytd-static-<@BUILD_TIME@>";
|
||||||
|
|
||||||
|
async function ThrowOnBadGateway(req)
|
||||||
|
{
|
||||||
|
const resp = await fetch(req);
|
||||||
|
if(resp.status === 502) throw new Error("Bad Gateway");
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('install',evt => {
|
||||||
|
evt.waitUntil(
|
||||||
|
caches.open(staticCacheName).then(cache =>{
|
||||||
|
cache.addAll(assets);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate',(evt)=>{
|
||||||
|
evt.waitUntil(
|
||||||
|
caches.keys().then(keys => {
|
||||||
|
return Promise.all(keys.filter(key => key !== staticCacheName).map(key => caches.delete(key)));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', evt => {
|
||||||
|
|
||||||
|
evt.respondWith(
|
||||||
|
|
||||||
|
caches.match(evt.request).then(cacheRes=>{
|
||||||
|
console.warn('SW cacheRes:', cacheRes);
|
||||||
|
return cacheRes || ThrowOnBadGateway(evt.request);
|
||||||
|
}).catch((err)=>{
|
||||||
|
console.warn('SW Fallback:', err.message);
|
||||||
|
if(evt.request.headers.has('HX-Request'))
|
||||||
|
{
|
||||||
|
|
||||||
|
return new Response("",{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'HX-Redirect': "/offline.html"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return caches.match('/offline.html');
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
73
Tesses.YouTubeDownloader.Server/res/site.webmanifest
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"short_name": "TYTD2025",
|
||||||
|
"name": "Tesses YouTubeDownloader 2025",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#ffb2be",
|
||||||
|
"background_color": "#241e1f",
|
||||||
|
"description": "A web based YouTube archiver to preserve videos that might be removed",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/tytd.svg",
|
||||||
|
"sizes": "192x192 256x256 384x384 512x512",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/tytd-128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/tytd-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/tytd-256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/tytd-384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/tytd-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/tytd-1024.png",
|
||||||
|
"sizes": "1024x1024",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Open Downloads",
|
||||||
|
"short_name": "Downloads",
|
||||||
|
"description": "Open Downloads page",
|
||||||
|
"url": "/downloads"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Open Installed Plugins",
|
||||||
|
"short_name": "Installed plugins",
|
||||||
|
"description": "Open the installed plugins page",
|
||||||
|
"url": "/plugins"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Open Download Plugins",
|
||||||
|
"short_name": "Download plugins",
|
||||||
|
"description": "Open the download plugins page",
|
||||||
|
"url": "/plugins-download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Open Settings",
|
||||||
|
"short_name": "Settings",
|
||||||
|
"description": "Open settings page",
|
||||||
|
"url": "/settings"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
Tesses.YouTubeDownloader.Server/res/tytd-1024.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-192.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-256.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-384.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-512.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
@@ -1,62 +0,0 @@
|
|||||||
func Components.DiscoverEntry(item)
|
|
||||||
{
|
|
||||||
switch(item.type)
|
|
||||||
{
|
|
||||||
case "video":
|
|
||||||
return
|
|
||||||
<div class="row">
|
|
||||||
<div class="min">
|
|
||||||
<img src={$"./video-thumbnail?v={Net.Http.UrlEncode(item.id)}"} width="144" width="120">
|
|
||||||
</div>
|
|
||||||
<div class="max">
|
|
||||||
<div class="col">
|
|
||||||
<div class="min">
|
|
||||||
<a target="_blank" href={$"./watch?v={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
|
|
||||||
</div>
|
|
||||||
<div class="min">
|
|
||||||
<span>{item.views} • {item.uploaded}</span>
|
|
||||||
</div>
|
|
||||||
<div class="min">
|
|
||||||
<each(var part : item.author)>
|
|
||||||
<if(TypeOf(part.navigationEndpoint) == "Dictionary")>
|
|
||||||
<true>
|
|
||||||
<a target="_blank" href={$"./channel?id={Net.Http.UrlEncode(part.navigationEndpoint.browseEndpoint.browseId)}"}>{part.text}</a>
|
|
||||||
|
|
||||||
</true>
|
|
||||||
<false>
|
|
||||||
<span>{part.text}</span>
|
|
||||||
</false>
|
|
||||||
</if>
|
|
||||||
</each>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
break;
|
|
||||||
case "channel":
|
|
||||||
return
|
|
||||||
<div class="row">
|
|
||||||
<div class="min">
|
|
||||||
<img src={$"./channel-thumbnail?id={Net.Http.UrlEncode(item.id)}"} width="144" width="144">
|
|
||||||
</div>
|
|
||||||
<div class="max">
|
|
||||||
<div class="col">
|
|
||||||
<div class="min">
|
|
||||||
<a target="_blank" href={$"./channel?id={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
|
|
||||||
</div>
|
|
||||||
<div class="min">
|
|
||||||
<span>{item.subs}</span>
|
|
||||||
</div>
|
|
||||||
<div class ="min">
|
|
||||||
<p class="min">{item.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
func Components.MusicAlbum(item)
|
|
||||||
{
|
|
||||||
return <div class="row">
|
|
||||||
<div class="min">
|
|
||||||
<img src={$"./album-art?id={Net.Http.UrlEncode(item.id)}"} width="200" height="200">
|
|
||||||
</div>
|
|
||||||
<div class="min">
|
|
||||||
<a href={$"./music-album?id={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
func Components.MusicArtist(item)
|
|
||||||
{
|
|
||||||
return <a href={$"./music-artist?id={Net.Http.UrlEncode(item.id)}"}>{item.name}</a>;
|
|
||||||
}
|
|
||||||
@@ -20,11 +20,19 @@ func Components.PersonalListDescription(tytd,name,editing)
|
|||||||
</form>
|
</form>
|
||||||
</true>
|
</true>
|
||||||
<false>
|
<false>
|
||||||
|
<dialog id="dialog">
|
||||||
|
<h5>The url</h5>
|
||||||
|
<div><a id="the_playlist_url" href=""></a></div>
|
||||||
|
<nav class="right-align no-space">
|
||||||
|
<button data-ui="#dialog" class="transparent link">OK</button>
|
||||||
|
</nav>
|
||||||
|
</dialog>
|
||||||
<div class="row" id="description">
|
<div class="row" id="description">
|
||||||
<div class="max">
|
<div class="max">
|
||||||
<plink(description)>
|
<plink(description)>
|
||||||
</div>
|
</div>
|
||||||
<div class="min">
|
<div class="min">
|
||||||
|
<button onclick="getPersonalTempLink()"><i>link</i></button>
|
||||||
<button hx-get={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-target="#description" hx-swap="outerHTML"><i>edit</i></button>
|
<button hx-get={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-target="#description" hx-swap="outerHTML"><i>edit</i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
var progress=0;
|
|
||||||
func Components.Progress(tytd)
|
func Components.Progress(tytd)
|
||||||
{
|
{
|
||||||
var vid = tytd.CurrentVideo;
|
var progress = tytd.GetProgress();
|
||||||
|
|
||||||
var html = <div hx-trigger="every 1500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
|
var html = <div hx-trigger="every 4500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
|
||||||
<h2><a hx-target="body" hx-push-url="true" hx-get={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"} href={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"}>{vid.Title}</a></h2>
|
<h2><a hx-target="body" hx-push-url="true" hx-get={$"./watch?v={Net.Http.UrlEncode(progress.CurrentVideo.VideoId)}"} href={$"./watch?v={Net.Http.UrlEncode(progress.CurrentVideo.VideoId)}"}>{progress.CurrentVideo.Title}</a></h2>
|
||||||
<h4><a hx-target="body" hx-push-url="true" hx-get={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"}>{vid.Channel}</a></h4>
|
<h4><a hx-target="body" hx-push-url="true" hx-get={$"./channel?id={Net.Http.UrlEncode(progress.CurrentVideo.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(progress.CurrentVideo.ChannelId)}"}>{progress.CurrentVideo.Channel}</a></h4>
|
||||||
<progress class="wavy light-green-text" value={tytd.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
|
<progress class="wavy light-green-text" value={progress.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
|
||||||
</div>;
|
</div>;
|
||||||
progress++;
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
func Components.QueueSZ(tytd)
|
||||||
|
{
|
||||||
|
var vid = tytd.CurrentVideo;
|
||||||
|
tytd.Mutex.Lock();
|
||||||
|
|
||||||
|
|
||||||
|
var html = <b hx-trigger="every 10000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML">{tytd.VideoQueueCount}</b>;
|
||||||
|
|
||||||
|
tytd.Mutex.Unlock();
|
||||||
|
return html;
|
||||||
|
}
|
||||||
@@ -1,44 +1,68 @@
|
|||||||
func Components.Shell(title, html, page, $mypage)
|
func Components.Shell(title, html, page, $mypage)
|
||||||
{
|
{
|
||||||
|
|
||||||
if(TypeOf(mypage) != "Path")
|
if(TypeOf(mypage) != "Path")
|
||||||
mypage = /;
|
mypage = /;
|
||||||
|
|
||||||
var index = (/).MakeRelative(mypage).ToString();
|
var index = (/).MakeRelative(mypage).ToString();
|
||||||
if(index == "") index = "./";
|
if(index == "") index = "./";
|
||||||
|
|
||||||
|
const service_worker_script_path = (/"service_worker.js").MakeRelative(mypage).ToString();
|
||||||
|
const service_worker_script = $"<script>
|
||||||
|
if(\"serviceWorker\" in navigator) {123.ToChar()}
|
||||||
|
navigator.serviceWorker.register({Json.Encode(service_worker_script_path)})
|
||||||
|
.then(()=>console.log('Service worker registered'))
|
||||||
|
.catch(()=>console.log('service work not registered'));
|
||||||
|
{125.ToChar()}
|
||||||
|
</script>";
|
||||||
|
|
||||||
var pages = [
|
var pages = [
|
||||||
{
|
{
|
||||||
text="Home",
|
text="Home",
|
||||||
icon="home",
|
icon="home",
|
||||||
href=index,
|
href=index,
|
||||||
classStr=""
|
classStr="",
|
||||||
|
badge = ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text="Queue",
|
||||||
|
icon="downloading",
|
||||||
|
href=(/"queue").MakeRelative(mypage).ToString(),,
|
||||||
|
classStr="",
|
||||||
|
badge=<b hx-trigger="every 1000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML">
|
||||||
|
?
|
||||||
|
</b>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text = "Downloads",
|
text = "Downloads",
|
||||||
icon = "download",
|
icon = "download",
|
||||||
href=(/"downloads").MakeRelative(mypage).ToString(),
|
href=(/"downloads").MakeRelative(mypage).ToString(),
|
||||||
classStr=""
|
classStr="",
|
||||||
|
badge=""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text = "Plugins",
|
text = "Plugins",
|
||||||
icon = "extension",
|
icon = "extension",
|
||||||
href= (/"plugins").MakeRelative(mypage).ToString(),
|
href= (/"plugins").MakeRelative(mypage).ToString(),
|
||||||
classStr=""
|
classStr="",
|
||||||
|
badge=""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text = "Settings",
|
text = "Settings",
|
||||||
icon = "settings",
|
icon = "settings",
|
||||||
href= (/"settings").MakeRelative(mypage).ToString(),
|
href= (/"settings").MakeRelative(mypage).ToString(),
|
||||||
classStr=""
|
classStr="",
|
||||||
|
badge=""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text = "Api",
|
text = "Api",
|
||||||
icon = "api",
|
icon = "api",
|
||||||
href = (/"api").MakeRelative(mypage).ToString(),
|
href = (/"api").MakeRelative(mypage).ToString(),
|
||||||
classStr=""
|
classStr="",
|
||||||
|
badge=""
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
pages[page].classStr = "active";
|
pages[page].classStr = " primary-container";
|
||||||
return <!DOCTYPE html>
|
return <!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -47,19 +71,66 @@ func Components.Shell(title, html, page, $mypage)
|
|||||||
<title>TYTD - {title}</title>
|
<title>TYTD - {title}</title>
|
||||||
<link rel="stylesheet" href={(/"beer.min.css").MakeRelative(mypage).ToString()}>
|
<link rel="stylesheet" href={(/"beer.min.css").MakeRelative(mypage).ToString()}>
|
||||||
<link rel="stylesheet" href={(/"theme.css").MakeRelative(mypage).ToString()}>
|
<link rel="stylesheet" href={(/"theme.css").MakeRelative(mypage).ToString()}>
|
||||||
|
<link rel="manifest" href={(/"site.webmanifest").MakeRelative(mypage).ToString()}>
|
||||||
|
|
||||||
<script src={(/"htmx.min.js").MakeRelative(mypage).ToString()} defer></script>
|
<script src={(/"htmx.min.js").MakeRelative(mypage).ToString()} defer></script>
|
||||||
|
|
||||||
|
<script src={(/"offline.js").MakeRelative(mypage).ToString()} defer></script>
|
||||||
<script type="module" src={(/"beer.min.js").MakeRelative(mypage).ToString()} defer></script>
|
<script type="module" src={(/"beer.min.js").MakeRelative(mypage).ToString()} defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body hx-indicator="#loading-indicator">
|
<body hx-indicator="#loading-indicator">
|
||||||
|
<dialog class="left" id="leftpane">
|
||||||
<nav class="bottom">
|
<header>
|
||||||
|
<nav>
|
||||||
|
<img class="circle large" src="/tytd-128.png">
|
||||||
|
<h6 class="max">TYTD2025</h6>
|
||||||
|
<button hx-on:click="ui('#leftpane')" class="transparent circle large">
|
||||||
|
<i>close</i>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class="space"></div>
|
||||||
|
<ul class="list">
|
||||||
|
<each(var page : pages)>
|
||||||
|
|
||||||
|
<li class={$"wave round{page.classStr}"} hx-target="body" hx-push-url="true" hx-get={page.href}>
|
||||||
|
<i>{page.icon}</i>
|
||||||
|
<span class="max">{page.text}</span>
|
||||||
|
<raw(page.badge)>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</each>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</dialog>
|
||||||
|
<header class="fixed primary-container">
|
||||||
|
<nav>
|
||||||
|
<button hx-on:click="ui('#leftpane')" class="circle transparent">
|
||||||
|
<i>menu</i>
|
||||||
|
</button>
|
||||||
|
<h6 class="max">TYTD2025</h6>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
/*<nav class="bottom">
|
||||||
<each(var page : pages)>
|
<each(var page : pages)>
|
||||||
<a hx-get={page.href} hx-target="body" hx-push-url="true" class={page.classStr}>
|
<a hx-get={page.href} hx-target="body" hx-push-url="true" class={page.classStr}>
|
||||||
<i>{page.icon}</i>
|
<i>{page.icon}</i>
|
||||||
|
<if(page.text == "Home")>
|
||||||
|
<true>
|
||||||
|
<span hx-trigger="every 1000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML" class="badge">
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
<div>{page.text}</div>
|
<div>{page.text}</div>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</each>
|
</each>
|
||||||
</nav>
|
</nav>*/
|
||||||
|
|
||||||
<main class="responsive">
|
<main class="responsive">
|
||||||
<raw(html)>
|
<raw(html)>
|
||||||
@@ -69,7 +140,7 @@ func Components.Shell(title, html, page, $mypage)
|
|||||||
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
|
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
|
||||||
<img class="responsive" src="./tytd.svg">
|
<img class="responsive" src="./tytd.svg">
|
||||||
</div>
|
</div>
|
||||||
|
<raw(service_worker_script)>
|
||||||
</body>
|
</body>
|
||||||
</html>;
|
</html>;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const BUILD_TIME = comptime DateTime.NowEpoch;
|
||||||
|
|
||||||
var TYTDResources = [
|
var TYTDResources = [
|
||||||
{path="/beer.min.css",value=embed("beer.min.css")},
|
{path="/beer.min.css",value=embed("beer.min.css")},
|
||||||
{path="/beer.min.js",value=embed("beer.min.js")},
|
{path="/beer.min.js",value=embed("beer.min.js")},
|
||||||
@@ -8,12 +10,31 @@ var TYTDResources = [
|
|||||||
{path="/htmx.min.js",value=embed("htmx.min.js")},
|
{path="/htmx.min.js",value=embed("htmx.min.js")},
|
||||||
{path="/favicon.ico",value=embed("favicon.ico")},
|
{path="/favicon.ico",value=embed("favicon.ico")},
|
||||||
{path="/tytd.svg",value=embed("tytd.svg")},
|
{path="/tytd.svg",value=embed("tytd.svg")},
|
||||||
|
{path="/tytd-128.png",value=embed("tytd-128.png")},
|
||||||
|
{path="/tytd-192.png",value=embed("tytd-192.png")},
|
||||||
|
{path="/tytd-256.png",value=embed("tytd-256.png")},
|
||||||
|
{path="/tytd-384.png",value=embed("tytd-384.png")},
|
||||||
|
{path="/tytd-512.png",value=embed("tytd-512.png")},
|
||||||
|
{path="/tytd-1024.png",value=embed("tytd-1024.png")},
|
||||||
{path="/loading-indicator.svg",value=embed("loading-indicator.svg")},
|
{path="/loading-indicator.svg",value=embed("loading-indicator.svg")},
|
||||||
{path="/theme.css",value=embed("theme.css")},
|
{path="/theme.css",value=embed("theme.css")},
|
||||||
{path="/video.min.js",value=embed("video.min.js")},
|
{path="/video.min.js",value=embed("video.min.js")},
|
||||||
{path="/video-js.css",value=embed("video-js.css")},
|
{path="/video-js.css",value=embed("video-js.css")},
|
||||||
{path="/wavy.svg",value=embed("wavy.svg")}
|
{path="/wavy.svg",value=embed("wavy.svg")},
|
||||||
|
{path="/site.webmanifest",value=embed("site.webmanifest")},
|
||||||
|
{path="/offline-progress.html",value=embed("offline-progress.html")},
|
||||||
|
{path="/offline.html",value=embed("offline.html")},
|
||||||
|
{path="/offline.js", value=embed("offline.js")},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const fileNames = [];
|
||||||
|
each(var item : TYTDResources)
|
||||||
|
{
|
||||||
|
fileNames.Add(item.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const service_worker_str = embed("service_worker.js").ToString().Replace("[\"<@ASSETS@>\"]",Json.Encode(fileNames)).Replace("<@BUILD_TIME@>",BUILD_TIME.ToString());
|
||||||
|
|
||||||
var times=1;
|
var times=1;
|
||||||
|
|
||||||
class TYTDApp {
|
class TYTDApp {
|
||||||
@@ -29,21 +50,71 @@ class TYTDApp {
|
|||||||
|
|
||||||
public TYTDApp()
|
public TYTDApp()
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"Built at {new DateTime(BUILD_TIME).ToString()}");
|
||||||
var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir());
|
var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir());
|
||||||
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir()));
|
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir()));
|
||||||
this.TYTD.Start();
|
this.TYTD.Start();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Handle(ctx)
|
public Handle(ctx)
|
||||||
{
|
{
|
||||||
|
if(ctx.Path == "/api/v1/resolutions.json")
|
||||||
|
{
|
||||||
|
ctx.SendJson(Resolution.Resolutions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(ctx.Path == "/service_worker.js" || fileNames.Contains(ctx.Path))
|
||||||
|
{
|
||||||
|
const inm=ctx.RequestHeaders.TryGetFirst("If-None-Match");
|
||||||
|
if(TypeIsString(inm))
|
||||||
|
{
|
||||||
|
const strs = inm.Split(", ");
|
||||||
|
each(var item : strs)
|
||||||
|
{
|
||||||
|
if(item == $"W/\"{BUILD_TIME}\"" || item == $"\"{BUILD_TIME}\"")
|
||||||
|
{
|
||||||
|
ctx.StatusCode = 304;
|
||||||
|
ctx.WriteHeaders();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.WithHeader("ETag",$"W/\"{BUILD_TIME}\"");
|
||||||
|
}
|
||||||
|
if(ctx.Path == "/service_worker.js")
|
||||||
|
{
|
||||||
|
ctx.WithMimeType(Net.Http.MimeType("service_worker.js")).SendText(service_worker_str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(ctx.Path == "/api/v1/auth")
|
||||||
|
{
|
||||||
|
if(ctx.Method == "POST")
|
||||||
|
{
|
||||||
|
const req=ctx.ReadJson();
|
||||||
|
const result = this.TYTD.Auth(req.username, req.password);
|
||||||
|
if(result)
|
||||||
|
{
|
||||||
|
ctx.SendJson({
|
||||||
|
success = true,
|
||||||
|
flags = result.flags
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctx.SendJson({
|
||||||
|
success=false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if(ctx.Path == "/api/v1/login")
|
if(ctx.Path == "/api/v1/login")
|
||||||
{
|
{
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
const req=ctx.ReadJson();
|
const req=ctx.ReadJson();
|
||||||
const result = this.TYTD.Login(req.username, req.password);
|
const result = this.TYTD.Login(req.username, req.password, false);
|
||||||
if(result)
|
if(result)
|
||||||
{
|
{
|
||||||
ctx.SendJson({
|
ctx.SendJson({
|
||||||
@@ -68,16 +139,19 @@ class TYTDApp {
|
|||||||
{
|
{
|
||||||
var incorrect=false;
|
var incorrect=false;
|
||||||
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
||||||
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
|
const username = ctx.BodyParams.TryGetFirst("username") ?? "";
|
||||||
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
const password = ctx.BodyParams.TryGetFirst("password") ?? "";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
const result = this.TYTD.Login(username, password);
|
const result = this.TYTD.Login(username, password,true);
|
||||||
if(result)
|
if(result)
|
||||||
{
|
{
|
||||||
ctx.StatusCode = 303;
|
ctx.StatusCode = 303;
|
||||||
ctx.WithHeader("Set-Cookie",$"Session={result}; SameSite=Strict").SendRedirect("/");
|
var date = new DateTime(DateTime.NowEpoch + UserFlags.Expires);
|
||||||
|
ctx.WithHeader("Set-Cookie",$"Session={result}; SameSite=Lax; Expires={date.ToHttpDate()}; HttpOnly").SendRedirect(redirect);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else incorrect=true;
|
else incorrect=true;
|
||||||
@@ -97,6 +171,29 @@ class TYTDApp {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(ctx.Path == "/progress")
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/html").SendText(
|
||||||
|
<div>
|
||||||
|
<h1>You have been logged out</h1>
|
||||||
|
<a href="./login">Login</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(ctx.Path == "/queue-size")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(ctx.Path == "/sso")
|
||||||
|
{
|
||||||
|
const app = ctx.QueryParams.TryGetFirst("app") ?? "";
|
||||||
|
const token = ctx.QueryParams.TryGetFirst("token") ?? "";
|
||||||
|
const path = $"/sso?app={Net.Http.UrlEncode(app)}&token={Net.Http.UrlEncode(token)}";
|
||||||
|
ctx.StatusCode = 307;
|
||||||
|
ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(path)}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
ctx.StatusCode = 307;
|
ctx.StatusCode = 307;
|
||||||
ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}");
|
ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}");
|
||||||
return true;
|
return true;
|
||||||
@@ -121,16 +218,16 @@ class TYTDApp {
|
|||||||
{
|
{
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
const from = ctx.QueryParams.TryGetFirstInt("from");
|
const from = ctx.BodyParams.TryGetFirstInt("from");
|
||||||
switch(from)
|
switch(from)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
{
|
{
|
||||||
const enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins");
|
const enablePlugins = ctx.BodyParams.GetFirstBoolean("enablePlugins");
|
||||||
const tag = ctx.QueryParams.TryGetFirst("tag") ?? "UnknownPC";
|
const tag = ctx.BodyParams.TryGetFirst("tag") ?? "UnknownPC";
|
||||||
const pollHours = ctx.QueryParams.TryGetFirstInt("pollHours") ?? 3;
|
const pollHours = ctx.BodyParams.TryGetFirstInt("pollHours") ?? 3;
|
||||||
const pollMinutes = ctx.QueryParams.TryGetFirstInt("pollMinutes") ?? 0;
|
const pollMinutes = ctx.BodyParams.TryGetFirstInt("pollMinutes") ?? 0;
|
||||||
const pollSeconds = ctx.QueryParams.TryGetFirstInt("pollSeconds") ?? 0;
|
const pollSeconds = ctx.BodyParams.TryGetFirstInt("pollSeconds") ?? 0;
|
||||||
|
|
||||||
|
|
||||||
this.OOBE_STATE.tag = tag;
|
this.OOBE_STATE.tag = tag;
|
||||||
@@ -151,7 +248,7 @@ class TYTDApp {
|
|||||||
case 3:
|
case 3:
|
||||||
{
|
{
|
||||||
|
|
||||||
const createUser = ctx.QueryParams.GetFirstBoolean("user");
|
const createUser = (ctx.BodyParams.TryGetFirst("user") ?? "no")=="yes";
|
||||||
|
|
||||||
var seconds = this.OOBE_STATE.pollSeconds;
|
var seconds = this.OOBE_STATE.pollSeconds;
|
||||||
seconds += this.OOBE_STATE.pollMinutes * 60;
|
seconds += this.OOBE_STATE.pollMinutes * 60;
|
||||||
@@ -208,7 +305,6 @@ class TYTDApp {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ctx.Path == "/welcome")
|
if(ctx.Path == "/welcome")
|
||||||
{
|
{
|
||||||
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings";
|
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings";
|
||||||
@@ -245,39 +341,98 @@ class TYTDApp {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ctx.Path == "/")
|
if(ctx.Path == "/")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD));
|
ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/sso")
|
||||||
|
{
|
||||||
|
const app = ctx.QueryParams.TryGetFirst("app");
|
||||||
|
const token = ctx.QueryParams.TryGetFirst("token");
|
||||||
|
if(TypeIsString(app) && TypeIsString(token))
|
||||||
|
{
|
||||||
|
|
||||||
|
const sso = this.TYTD.GetSSO(app);
|
||||||
|
if(TypeIsDictionary(sso))
|
||||||
|
{
|
||||||
|
const user = this.TYTD.WhoAmI(ctx);
|
||||||
|
if(TypeIsDictionary(user) && user.flags != 0)
|
||||||
|
{
|
||||||
|
const postJson = {
|
||||||
|
username = user.username,
|
||||||
|
flags = user.flags,
|
||||||
|
token = token,
|
||||||
|
sso_app_key = sso.sso_app_key
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = Net.Http.MakeRequest(sso.service_auth_post, {
|
||||||
|
Method = "POST",
|
||||||
|
Body = Net.Http.TextHttpRequestBody(postJson.ToString(),"application/json")
|
||||||
|
});
|
||||||
|
|
||||||
|
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||||
|
{
|
||||||
|
ctx.StatusCode = 307;
|
||||||
|
ctx.SendRedirect($"{sso.service_auth_redirect}{Net.Http.UrlEncode(token)}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.StatusCode = 401;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/passwd")
|
else if(ctx.Path == "/passwd")
|
||||||
{
|
{
|
||||||
var incorrect=false;
|
var result = {success=true};
|
||||||
|
|
||||||
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
|
||||||
const newpassword = ctx.QueryParams.TryGetFirst("newpassword") ?? "";
|
|
||||||
|
|
||||||
const confirm = ctx.QueryParams.TryGetFirst("newpassword") ?? "";
|
const redirect = ctx.BodyParams.TryGetFirst("redirect") ?? "/login";
|
||||||
|
const password = ctx.BodyParams.TryGetFirst("password") ?? "";
|
||||||
|
const newpassword = ctx.BodyParams.TryGetFirst("newpassword") ?? "";
|
||||||
|
|
||||||
|
const confirm = ctx.BodyParams.TryGetFirst("confirm") ?? "";
|
||||||
|
const logout = ctx.BodyParams.GetFirstBoolean("logout");
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
this.TYTD.Passwd();
|
//public Passwd(ctx, oldPassword, newPassword, logout)
|
||||||
|
if(password != newpassword)
|
||||||
|
{
|
||||||
|
result = {success = false, reason = "Passwords do not match"};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
result = this.TYTD.Passwd(ctx, password, newpassword, logout);
|
||||||
|
|
||||||
|
if(result.success)
|
||||||
|
{
|
||||||
|
ctx.StatusCode = 307;
|
||||||
|
ctx.SendRedirect(redirect);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
|
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, result.success ? null : result.reason));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(ctx.Path == "/newuser")
|
else if(ctx.Path == "/newuser")
|
||||||
{
|
{
|
||||||
var error = null;
|
var error = null;
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
const username = ctx.QueryParams.TryGetFirst("username");
|
const username = ctx.BodyParams.TryGetFirst("username");
|
||||||
const password = ctx.QueryParams.TryGetFirst("password");
|
const password = ctx.BodyParams.TryGetFirst("password");
|
||||||
const confirm = ctx.QueryParams.TryGetFirst("confirm");
|
const confirm = ctx.BodyParams.TryGetFirst("confirm");
|
||||||
const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin");
|
const isAdmin = ctx.BodyParams.GetFirstBoolean("isAdmin");
|
||||||
const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins");
|
const canUsePlugins = ctx.BodyParams.GetFirstBoolean("canUsePlugins");
|
||||||
const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins");
|
const canManagePlugins = ctx.BodyParams.GetFirstBoolean("canManagePlugins");
|
||||||
const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB");
|
const canDownloadDB = ctx.BodyParams.GetFirstBoolean("canDownloadDB");
|
||||||
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
|
||||||
var flags = 0;
|
var flags = 0;
|
||||||
if(isAdmin) flags |= UserFlags.AdminFlag;
|
if(isAdmin) flags |= UserFlags.AdminFlag;
|
||||||
if(canUsePlugins) flags |= UserFlags.PluginFlag;
|
if(canUsePlugins) flags |= UserFlags.PluginFlag;
|
||||||
@@ -299,12 +454,6 @@ class TYTDApp {
|
|||||||
else {
|
else {
|
||||||
error = this.TYTD.CreateAccount(ctx, username, password, flags);
|
error = this.TYTD.CreateAccount(ctx, username, password, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!TypeIsString(error))
|
|
||||||
{
|
|
||||||
ctx.StatusCode=303;
|
|
||||||
ctx.SendRedirect(redirect);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.NewUser(error));
|
ctx.WithMimeType("text/html").SendText(Pages.NewUser(error));
|
||||||
return true;
|
return true;
|
||||||
@@ -340,6 +489,36 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/api/v1/register_sso")
|
||||||
|
{
|
||||||
|
if(ctx.Method!="POST")
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("application/json").SendJson({
|
||||||
|
success=false,
|
||||||
|
reason = "Method must be post",
|
||||||
|
type = "method"
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx)))
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("application/json").SendJson({
|
||||||
|
success=false,
|
||||||
|
reason = "You are either not logged in or not admin",
|
||||||
|
type ="auth"
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = ctx.ReadJson();
|
||||||
|
|
||||||
|
const resp = this.TYTD.RegisterSSO(json);
|
||||||
|
|
||||||
|
ctx.WithMimeType("application/json").SendJson(
|
||||||
|
resp
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/api/v1/download")
|
else if(ctx.Path == "/api/v1/download")
|
||||||
{
|
{
|
||||||
var v = ctx.QueryParams.TryGetFirst("v");
|
var v = ctx.QueryParams.TryGetFirst("v");
|
||||||
@@ -361,16 +540,30 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/api/v1/progress_ex.json")
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetProgressEx());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/api/v1/progress.json")
|
else if(ctx.Path == "/api/v1/progress.json")
|
||||||
{
|
{
|
||||||
var jo = {
|
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetProgress());
|
||||||
CurrentVideoProgress = this.TYTD.CurrentVideoProgress,
|
|
||||||
CurrentVideo = this.TYTD.CurrentVideo
|
|
||||||
|
|
||||||
};
|
|
||||||
ctx.WithMimeType("application/json").SendJson(jo);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/api/v1/add")
|
||||||
|
{
|
||||||
|
if(ctx.Method=="POST")
|
||||||
|
{
|
||||||
|
const json = ctx.ReadJson();
|
||||||
|
each(var item : json)
|
||||||
|
{
|
||||||
|
this.TYTD.DownloadItem(item.url,item.res);
|
||||||
|
}
|
||||||
|
ctx.StatusCode=204;
|
||||||
|
ctx.WriteHeaders();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if(ctx.Path == "/api/v1/video.json")
|
else if(ctx.Path == "/api/v1/video.json")
|
||||||
{
|
{
|
||||||
var id = ctx.QueryParams.TryGetFirst("v");
|
var id = ctx.QueryParams.TryGetFirst("v");
|
||||||
@@ -400,6 +593,21 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count));
|
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/api/v1/queue.json")
|
||||||
|
{
|
||||||
|
if(ctx.Method == "GET")
|
||||||
|
{
|
||||||
|
var page = ctx.QueryParams.TryGetFirstInt("page");
|
||||||
|
var count = ctx.QueryParams.TryGetFirstInt("count");
|
||||||
|
if(TypeOf(page)!="Long") page = 1;
|
||||||
|
if(TypeOf(count)!="Long") count = 20;
|
||||||
|
|
||||||
|
|
||||||
|
var result = { entries = TYTD.GetQueueItems(page-1,count)};
|
||||||
|
ctx.WithMimeType("application/json").SendJson(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if(ctx.Path == "/api/v1/downloads.json")
|
else if(ctx.Path == "/api/v1/downloads.json")
|
||||||
{
|
{
|
||||||
var q = ctx.QueryParams.TryGetFirst("q");
|
var q = ctx.QueryParams.TryGetFirst("q");
|
||||||
@@ -435,10 +643,34 @@ class TYTDApp {
|
|||||||
|
|
||||||
if(!this.TYTD.SendDatabase(ctx))
|
if(!this.TYTD.SendDatabase(ctx))
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized","<h1>You are not authorized to download the database</h1>",3,/"api/v1/database.db"));
|
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized","<h1>You are not authorized to download the database</h1>",4,/"api/v1/database.db"));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/api/v1/personal_tmp_link")
|
||||||
|
{
|
||||||
|
const name = ctx.QueryParams.TryGetFirst("name");
|
||||||
|
if(TypeIsString(name))
|
||||||
|
{
|
||||||
|
const ents = this.TYTD.GetPersonalListTempUrl(name);
|
||||||
|
if(TypeIsString(ents))
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/plain").SendText(ents);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(ctx.Path == "/api/v1/manifest.json")
|
||||||
|
{
|
||||||
|
const v = ctx.QueryParams.TryGetFirst("v");
|
||||||
|
if(TypeIsString(v))
|
||||||
|
{
|
||||||
|
const resp=this.TYTD.ManifestRequest(v);
|
||||||
|
ctx.WithMimeType("application/json").SendJson(resp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if(ctx.Path == "/api/v1/personal")
|
else if(ctx.Path == "/api/v1/personal")
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
@@ -572,13 +804,31 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
else if(ctx.Method == "POST")
|
else if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
var description =ctx.QueryParams.TryGetFirst("description");
|
var description =ctx.BodyParams.TryGetFirst("description");
|
||||||
this.TYTD.SetPersonalListDescription(name,description);
|
this.TYTD.SetPersonalListDescription(name,description);
|
||||||
ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,false));
|
ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,false));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/queue")
|
||||||
|
{
|
||||||
|
|
||||||
|
if(ctx.Method == "DELETE")
|
||||||
|
{
|
||||||
|
const id = ctx.QueryParams.TryGetFirstInt("id");
|
||||||
|
if(TypeIsLong(id))
|
||||||
|
{
|
||||||
|
this.TYTD.QueueRemoveItem(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.TYTD.QueueClear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.Queue(this.TYTD,ctx));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/downloads")
|
else if(ctx.Path == "/downloads")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx));
|
||||||
@@ -599,7 +849,7 @@ class TYTDApp {
|
|||||||
{
|
{
|
||||||
if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx)))
|
if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx)))
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,3));
|
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,4));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const user = ctx.QueryParams.TryGetFirst("user");
|
const user = ctx.QueryParams.TryGetFirst("user");
|
||||||
@@ -612,10 +862,10 @@ class TYTDApp {
|
|||||||
{
|
{
|
||||||
if(whoami.username != user)
|
if(whoami.username != user)
|
||||||
{
|
{
|
||||||
const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin");
|
const isAdmin = ctx.BodyParams.GetFirstBoolean("isAdmin");
|
||||||
const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins");
|
const canUsePlugins = ctx.BodyParams.GetFirstBoolean("canUsePlugins");
|
||||||
const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins");
|
const canManagePlugins = ctx.BodyParams.GetFirstBoolean("canManagePlugins");
|
||||||
const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB");
|
const canDownloadDB = ctx.BodyParams.GetFirstBoolean("canDownloadDB");
|
||||||
var flags = 0;
|
var flags = 0;
|
||||||
if(isAdmin) flags |= UserFlags.AdminFlag;
|
if(isAdmin) flags |= UserFlags.AdminFlag;
|
||||||
if(canUsePlugins) flags |= UserFlags.PluginFlag;
|
if(canUsePlugins) flags |= UserFlags.PluginFlag;
|
||||||
@@ -677,7 +927,7 @@ class TYTDApp {
|
|||||||
{
|
{
|
||||||
if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx)))
|
if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx)))
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,3));
|
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,4));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
@@ -690,11 +940,11 @@ class TYTDApp {
|
|||||||
tag
|
tag
|
||||||
enablePlugins
|
enablePlugins
|
||||||
*/
|
*/
|
||||||
var enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins");
|
var enablePlugins = ctx.BodyParams.GetFirstBoolean("enablePlugins");
|
||||||
var tag = ctx.QueryParams.TryGetFirst("tag");
|
var tag = ctx.BodyParams.TryGetFirst("tag");
|
||||||
var hours = ctx.QueryParams.TryGetFirstInt("hours");
|
var hours = ctx.BodyParams.TryGetFirstInt("hours");
|
||||||
var minutes = ctx.QueryParams.TryGetFirstInt("minutes");
|
var minutes = ctx.BodyParams.TryGetFirstInt("minutes");
|
||||||
var seconds = ctx.QueryParams.TryGetFirstInt("seconds");
|
var seconds = ctx.BodyParams.TryGetFirstInt("seconds");
|
||||||
|
|
||||||
if(TypeOf(tag) == "String" && TypeOf(hours) == "Long" && TypeOf(minutes) == "Long" && TypeOf(seconds) == "Long")
|
if(TypeOf(tag) == "String" && TypeOf(hours) == "Long" && TypeOf(minutes) == "Long" && TypeOf(seconds) == "Long")
|
||||||
{
|
{
|
||||||
@@ -714,6 +964,44 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if(ctx.Path == "/watch_videos")
|
||||||
|
{
|
||||||
|
|
||||||
|
if(ctx.Method=="GET")
|
||||||
|
{
|
||||||
|
const video_ids = ctx.QueryParams.TryGetFirst("video_ids");
|
||||||
|
|
||||||
|
if(TypeIsString(video_ids))
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.YouTubeAnonyPlaylist(video_ids));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(ctx.Method=="POST")
|
||||||
|
{
|
||||||
|
const video_ids = ctx.BodyParams.TryGetFirst("video_ids");
|
||||||
|
|
||||||
|
if(TypeIsString(video_ids))
|
||||||
|
{
|
||||||
|
const name = ctx.BodyParams.TryGetFirst("name");
|
||||||
|
if(TypeIsString(name))
|
||||||
|
{
|
||||||
|
const nameParts=video_ids.Split(",");
|
||||||
|
//Console.WriteLine(nameParts);
|
||||||
|
each(var item : nameParts)
|
||||||
|
{
|
||||||
|
|
||||||
|
this.TYTD.AddToPersonalList(name,item);
|
||||||
|
}
|
||||||
|
//Console.WriteLine(name);
|
||||||
|
ctx.SendRedirect($"/list?name={Net.Http.UrlEncode(name)}",303);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
else if(ctx.Path == "/playlist")
|
else if(ctx.Path == "/playlist")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx));
|
||||||
@@ -726,9 +1014,9 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
else if(ctx.Path == "/add")
|
else if(ctx.Path == "/add")
|
||||||
{
|
{
|
||||||
var url = ctx.QueryParams.TryGetFirst("url");
|
var url = ctx.BodyParams.TryGetFirst("url");
|
||||||
var action =ctx.QueryParams.TryGetFirst("action");
|
var action =ctx.BodyParams.TryGetFirst("action");
|
||||||
var res = ctx.QueryParams.TryGetFirst("res");
|
var res = ctx.BodyParams.TryGetFirst("res");
|
||||||
if(action == "add")
|
if(action == "add")
|
||||||
{
|
{
|
||||||
this.TYTD.DownloadItem(url,res);
|
this.TYTD.DownloadItem(url,res);
|
||||||
@@ -756,9 +1044,9 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
else if(ctx.Path == "/add-video")
|
else if(ctx.Path == "/add-video")
|
||||||
{
|
{
|
||||||
var url = ctx.QueryParams.TryGetFirst("url");
|
var url = ctx.BodyParams.TryGetFirst("url");
|
||||||
var action =ctx.QueryParams.TryGetFirst("action");
|
var action =ctx.BodyParams.TryGetFirst("action");
|
||||||
var res = ctx.QueryParams.TryGetFirst("res");
|
var res = ctx.BodyParams.TryGetFirst("res");
|
||||||
if(action == "add")
|
if(action == "add")
|
||||||
{
|
{
|
||||||
this.TYTD.DownloadItem(url,res);
|
this.TYTD.DownloadItem(url,res);
|
||||||
@@ -779,10 +1067,10 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
else if(ctx.Path == "/add-to-list")
|
else if(ctx.Path == "/add-to-list")
|
||||||
{
|
{
|
||||||
var id = ctx.QueryParams.TryGetFirst("id");
|
var id = ctx.BodyParams.TryGetFirst("id");
|
||||||
if(TypeOf(id) != "String") id = "";
|
if(TypeOf(id) != "String") id = "";
|
||||||
|
|
||||||
var name = ctx.QueryParams.TryGetFirst("name");
|
var name = ctx.BodyParams.TryGetFirst("name");
|
||||||
if(TypeOf(name) != "String") name = "";
|
if(TypeOf(name) != "String") name = "";
|
||||||
|
|
||||||
if(name == "")
|
if(name == "")
|
||||||
@@ -802,6 +1090,12 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("text/html").SendText(Components.Progress(this.TYTD));
|
ctx.WithMimeType("text/html").SendText(Components.Progress(this.TYTD));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/queue-size")
|
||||||
|
{
|
||||||
|
|
||||||
|
ctx.WithMimeType("text/html").SendText(Components.QueueSZ(this.TYTD));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/plugins")
|
else if(ctx.Path == "/plugins")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx));
|
||||||
@@ -817,9 +1111,9 @@ class TYTDApp {
|
|||||||
{
|
{
|
||||||
if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false;
|
if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false;
|
||||||
var data = {
|
var data = {
|
||||||
name = ctx.QueryParams.TryGetFirst("name"),
|
name = ctx.BodyParams.TryGetFirst("name"),
|
||||||
version = ctx.QueryParams.TryGetFirst("version"),
|
version = ctx.BodyParams.TryGetFirst("version"),
|
||||||
action = ctx.QueryParams.TryGetFirst("action")
|
action = ctx.BodyParams.TryGetFirst("action")
|
||||||
};
|
};
|
||||||
|
|
||||||
var mustConfirm = false;
|
var mustConfirm = false;
|
||||||
|
|||||||
@@ -72,5 +72,5 @@ func Pages.Admin(tytd,ctx)
|
|||||||
|
|
||||||
</form>;
|
</form>;
|
||||||
|
|
||||||
return Components.Shell("Admin Settings",html ,3);
|
return Components.Shell("Admin Settings",html ,4);
|
||||||
}
|
}
|
||||||
@@ -8,5 +8,5 @@ func Pages.Api()
|
|||||||
</a>
|
</a>
|
||||||
</ul>
|
</ul>
|
||||||
</null>;
|
</null>;
|
||||||
return Components.Shell("Api",html,4);
|
return Components.Shell("Api",html,5);
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ func Pages.ApiV1()
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</each>
|
</each>
|
||||||
</null>;
|
</null>;
|
||||||
return Components.Shell("Api V1",html,4);
|
return Components.Shell("Api V1",html,5);
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiv1_routes = Json.Decode(embed("apiv1_routes.json").ToString());
|
var apiv1_routes = Json.Decode(embed("apiv1_routes.json").ToString());
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
func Pages.ChangePassword(redirect,incorrect)
|
func Pages.ChangePassword(redirect,error)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const html = <null>
|
const html = <null>
|
||||||
<if(incorrect)>
|
<if(TypeIsString(error))>
|
||||||
<true>
|
<true>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<h5>The password could not be changed</h5>
|
<h5>{error}</h5>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</true>
|
</true>
|
||||||
</if>
|
</if>
|
||||||
@@ -45,5 +45,5 @@ const html = <null>
|
|||||||
</form>
|
</form>
|
||||||
</null>;
|
</null>;
|
||||||
|
|
||||||
return Components.Shell("Change your password",html,3);
|
return Components.Shell("Change your password",html,4);
|
||||||
}
|
}
|
||||||
@@ -35,5 +35,5 @@ func Pages.ChannelInfo(tytd,ctx)
|
|||||||
title = res.authorName;
|
title = res.authorName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Components.Shell($"Channel {title}",html,1);
|
return Components.Shell($"Channel {title}",html,2);
|
||||||
}
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
func Pages.Discover(tytd,ctx)
|
|
||||||
{
|
|
||||||
|
|
||||||
var q = ctx.QueryParams.TryGetFirst("q");
|
|
||||||
var res = null;
|
|
||||||
var q2 = "";
|
|
||||||
if(TypeOf(q) == "String")
|
|
||||||
{
|
|
||||||
q2 = q;
|
|
||||||
res = tytd.Discover(q);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var html = <null>
|
|
||||||
<form hx-get="./discover" hx-target="body" hx-push-url="true">
|
|
||||||
<div class="field large prefix round fill active">
|
|
||||||
<i class="front">search</i>
|
|
||||||
<input name="q" value={q2}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<if(res != null)>
|
|
||||||
<true>
|
|
||||||
<ul>
|
|
||||||
<each(var item : res.items)>
|
|
||||||
<li><raw(Components.DiscoverEntry(item))></li>
|
|
||||||
</each>
|
|
||||||
</ul>
|
|
||||||
<button class="responsive">More</button>
|
|
||||||
</true></null>;
|
|
||||||
|
|
||||||
|
|
||||||
return Components.Shell($"Discover {q2}",html,1);
|
|
||||||
}
|
|
||||||
@@ -289,5 +289,5 @@ func Pages.Downloads(tytd,ctx)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
return Components.Shell($"Downloads {q}",html,1);
|
return Components.Shell($"Downloads {q}",html,2);
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ func Pages.EditUserList(tytd, ctx)
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</null>;
|
</null>;
|
||||||
|
|
||||||
return Components.Shell("Edit users",html, 3);
|
return Components.Shell("Edit users",html, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
func Pages.EditUser(user)
|
func Pages.EditUser(user)
|
||||||
@@ -141,5 +141,5 @@ func Pages.EditUser(user)
|
|||||||
</if>;
|
</if>;
|
||||||
|
|
||||||
|
|
||||||
return Components.Shell("Edit user",html, 3);
|
return Components.Shell("Edit user",html, 4);
|
||||||
}
|
}
|
||||||
@@ -35,5 +35,5 @@ func Pages.List(tytd,ctx)
|
|||||||
</null>;
|
</null>;
|
||||||
|
|
||||||
|
|
||||||
return Components.Shell($"Personal List {name}",html,1);
|
return Components.Shell($"Personal List {name}",html,2);
|
||||||
}
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
func Pages.Artist(music,ctx)
|
|
||||||
{
|
|
||||||
|
|
||||||
var id = ctx.QueryParams.TryGetFirst("id");
|
|
||||||
var res = music.GetArtistsAlbums(id);
|
|
||||||
|
|
||||||
if(TypeOf(id) != "String")
|
|
||||||
{
|
|
||||||
return Components.Shell("Error",<h1>Artist id not specified</h1>,pages);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var html = <null>
|
|
||||||
<h1>{res.name}</h1>
|
|
||||||
<ul>
|
|
||||||
<each(var item : res.items)>
|
|
||||||
<li><raw(Components.MusicAlbum(item))></li>
|
|
||||||
</each>
|
|
||||||
</ul>
|
|
||||||
</null>;
|
|
||||||
|
|
||||||
|
|
||||||
return Components.Shell($"Artist {res.name}",html,2);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
func Pages.Artists(music,ctx)
|
|
||||||
{
|
|
||||||
|
|
||||||
var q = ctx.QueryParams.TryGetFirst("q");
|
|
||||||
var res = null;
|
|
||||||
var q2 = "";
|
|
||||||
if(TypeOf(q) == "String")
|
|
||||||
{
|
|
||||||
q2 = q;
|
|
||||||
res = music.GetArtists(q);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var html = <null>
|
|
||||||
<form hx-get="./music-artists" hx-target="body" hx-push-url="true">
|
|
||||||
<div class="field large prefix round fill active">
|
|
||||||
<i class="front">search</i>
|
|
||||||
<input name="q" value={q2}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<if(res != null)>
|
|
||||||
<true>
|
|
||||||
<ul>
|
|
||||||
<each(var item : res)>
|
|
||||||
<li><raw(Components.MusicArtist(item))></li>
|
|
||||||
</each>
|
|
||||||
</ul>
|
|
||||||
</true></null>;
|
|
||||||
|
|
||||||
|
|
||||||
return Components.Shell($"Search Artists {q2}",html,2);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
func Pages.Music(ctx)
|
|
||||||
{
|
|
||||||
var html = <ul>
|
|
||||||
<div><a href="./music-artists">Artists</a></div>
|
|
||||||
<div><a href="./music-downloaded">My Music</a></div>
|
|
||||||
</ul>;
|
|
||||||
return Components.Shell("Music",html,2);
|
|
||||||
}
|
|
||||||
@@ -81,5 +81,5 @@ func Pages.NewUser(error)
|
|||||||
</form>
|
</form>
|
||||||
</null>;
|
</null>;
|
||||||
|
|
||||||
return Components.Shell("New User",html ,3);
|
return Components.Shell("New User",html ,4);
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ func Pages.OobePage2(tytd,ctx)
|
|||||||
<div class="small-margin">Security</div>
|
<div class="small-margin">Security</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<form hx-get="./oobe" hx-target="body" hx-push-url="true">
|
<form hx-post="./oobe" hx-target="body" hx-push-url="true">
|
||||||
<input type="hidden" name="page" value="2">
|
<input type="hidden" name="page" value="2">
|
||||||
<div class="field suffix border round">
|
<div class="field suffix border round">
|
||||||
<select name="server">
|
<select name="server">
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ func Pages.OobePage3()
|
|||||||
</div>
|
</div>
|
||||||
<hr class="max">
|
<hr class="max">
|
||||||
<div class="center-align">
|
<div class="center-align">
|
||||||
<a href="./oobe?page=2" class="button circle small">2</a>
|
<a href="./oobe?page=2" class="button circle small">
|
||||||
|
<i>done</i>
|
||||||
|
</a>
|
||||||
<div class="small-margin">Plugins</div>
|
<div class="small-margin">Plugins</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="max">
|
<hr class="max">
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
func Pages.YouTubeAnonyPlaylist(video_ids)
|
||||||
|
{
|
||||||
|
const html = <null>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max"></div>
|
||||||
|
<div class="min">
|
||||||
|
<form method="POST" action="./watch_videos">
|
||||||
|
<input type="hidden" name="video_ids" value={video_ids}>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max">
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="text" name="name">
|
||||||
|
<label>Playlist Name</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min">
|
||||||
|
<button>Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="max"></div>
|
||||||
|
</div>
|
||||||
|
</null>;
|
||||||
|
return Components.Shell("Create playlist",html ,2);
|
||||||
|
}
|
||||||
@@ -35,5 +35,5 @@ func Pages.PlaylistInfo(tytd,ctx)
|
|||||||
title = res.authorName;
|
title = res.authorName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Components.Shell($"Playlist {title}",html,1);
|
return Components.Shell($"Playlist {title}",html,2);
|
||||||
}
|
}
|
||||||
@@ -75,5 +75,5 @@ func Pages.DownloadPlugins(tytd,ctx)
|
|||||||
|
|
||||||
</null>;
|
</null>;
|
||||||
|
|
||||||
return Components.Shell("Download plugins",html ,2);
|
return Components.Shell("Download plugins",html ,3);
|
||||||
}
|
}
|
||||||
@@ -20,5 +20,5 @@ func Pages.Plugins(tytd,ctx)
|
|||||||
</each>
|
</each>
|
||||||
</null>;
|
</null>;
|
||||||
|
|
||||||
return Components.Shell("Installed plugins", UserFlags.CanUsePlugins(userFlags) ? html : <h1>You can{"'"}t use plugins</h1> ,2);
|
return Components.Shell("Installed plugins", UserFlags.CanUsePlugins(userFlags) ? html : <h1>You can{"'"}t use plugins</h1> ,3);
|
||||||
}
|
}
|
||||||
71
Tesses.YouTubeDownloader.Server/src/pages/queue.tcross
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
func Pages.Queue(tytd,ctx)
|
||||||
|
{
|
||||||
|
var page = ctx.QueryParams.TryGetFirstInt("page");
|
||||||
|
if(TypeOf(page) != "Long") page = 1;
|
||||||
|
page--;
|
||||||
|
|
||||||
|
|
||||||
|
enumerable func getitems()
|
||||||
|
{
|
||||||
|
each(var item : tytd.GetQueueItems(page,10))
|
||||||
|
{
|
||||||
|
tytd.PutVideoInfoIfNotExists(item.videoId);
|
||||||
|
const info = tytd.GetVideo(item.videoId);
|
||||||
|
|
||||||
|
yield {
|
||||||
|
Title = $"{info.title}-{item.videoId}-{item.resolution}",
|
||||||
|
VideoId = item.videoId,
|
||||||
|
Id = item.id,
|
||||||
|
ChannelId = info.channelId,
|
||||||
|
Channel = info.author
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const html = <null>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="min">
|
||||||
|
<button hx-delete="./queue" hx-target="body" hx-push-url="true" hx-confirm="Are you sure you want to clear the queue?">Clear All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<each(var item : getitems())>
|
||||||
|
<div class="row">
|
||||||
|
<div class="min">
|
||||||
|
<img src={$"./api/v1/video-thumbnail?v={Net.Http.UrlEncode(item.VideoId)}"} width="144" width="120">
|
||||||
|
</div>
|
||||||
|
<div class="max">
|
||||||
|
<div class="col">
|
||||||
|
<div class="min">
|
||||||
|
{item.Title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min">
|
||||||
|
<a hx-get={$"./channel?id={Net.Http.UrlEncode(item.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(item.ChannelId)}"} hx-target="body" hx-push-url="true">{item.Channel}</a>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min">
|
||||||
|
<button class="circle" hx-delete={$"./queue?page={page+1}"} hx-vals={{id = item.Id}} hx-target="body" hx-push-url="true" hx-confirm="Are you sure you want to delete this?"><i>delete</i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</each>
|
||||||
|
|
||||||
|
<footer class="row center-align">
|
||||||
|
|
||||||
|
<button hx-get={$"./queue?page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./queue?page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</null>;
|
||||||
|
return Components.Shell("Queue",html,1);
|
||||||
|
}
|
||||||
@@ -15,5 +15,5 @@ func Pages.Settings(tytd,ctx)
|
|||||||
|
|
||||||
</fieldset>;
|
</fieldset>;
|
||||||
|
|
||||||
return Components.Shell("Settings",html ,3);
|
return Components.Shell("Settings",html ,4);
|
||||||
}
|
}
|
||||||
@@ -27,5 +27,5 @@ func Pages.VideoInfo(tytd,ctx)
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Components.Shell(vi != null ? vi.title : "Could not find video", html, 1);
|
return Components.Shell(vi != null ? vi.title : "Could not find video", html, 2);
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ func Pages.Welcome(redirect)
|
|||||||
const html = <null>
|
const html = <null>
|
||||||
<h1>You are all set</h1>
|
<h1>You are all set</h1>
|
||||||
<p>
|
<p>
|
||||||
This is Tesses YouTube Downloader 2025, a YouTube Downloader created in <a href="https://crosslang.tesseslanguage.com/">CrossLang</a>
|
This is Tesses YouTube Downloader 2025, a YouTube Downloader created in <a target="_blank" href="https://crosslang.tesseslanguage.com/">CrossLang</a>
|
||||||
|
|
||||||
<h4><i>home</i> Home</h4>
|
<h4><i>home</i> Home</h4>
|
||||||
Once you click the <b>{"I'm Done"}</b> button, you will be able to add YouTube videos to the downloader by using <i>add</i>, you can download the video from the server using <i>download</i> or view metadata/watch the video using <i>info</i> (this also will allow you to view playlist/channel contents)
|
Once you click the <b>{"I'm Done"}</b> button, you will be able to add YouTube videos to the downloader by using <i>add</i>, you can download the video from the server using <i>download</i> or view metadata/watch the video using <i>info</i> (this also will allow you to view playlist/channel contents)
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ func Pages.WhoAmI(row)
|
|||||||
<li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li>
|
<li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</null>;
|
</null>;
|
||||||
return Components.Shell("Settings",html ,3);
|
return Components.Shell("Settings",html ,4);
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "Tesses.YouTubeDownloader",
|
"name": "Tesses.YouTubeDownloader",
|
||||||
"version": "1.0.0.0-dev",
|
"version": "1.0.1.0-dev",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
"context": {
|
"context": {
|
||||||
"client": {
|
"client": {
|
||||||
"clientName": "ANDROID",
|
"clientName": "ANDROID",
|
||||||
"clientVersion": "19.28.35",
|
"clientVersion": "21.03.36",
|
||||||
"clientScreen": "WATCH",
|
"clientScreen": "WATCH",
|
||||||
"platform": "MOBILE",
|
"platform": "MOBILE",
|
||||||
"osName": "Android",
|
"osName": "Android",
|
||||||
"osVersion": "15",
|
"osVersion": "16",
|
||||||
"androidSdkVersion": 35,
|
"androidSdkVersion": 36,
|
||||||
"hl": "en-GB",
|
"hl": "en-GB",
|
||||||
"gl": "US",
|
"gl": "US",
|
||||||
"utcOffsetMinutes": 0
|
"utcOffsetMinutes": 0
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
"context": {
|
"context": {
|
||||||
"client": {
|
"client": {
|
||||||
"clientName": "ANDROID",
|
"clientName": "ANDROID",
|
||||||
"clientVersion": "19.28.35",
|
"clientVersion": "21.03.36",
|
||||||
"clientScreen": "WATCH",
|
"clientScreen": "WATCH",
|
||||||
"platform": "MOBILE",
|
"platform": "MOBILE",
|
||||||
"visitorData": "VISITOR_DATA",
|
"visitorData": "VISITOR_DATA",
|
||||||
"osName": "Android",
|
"osName": "Android",
|
||||||
"osVersion": "15",
|
"osVersion": "16",
|
||||||
"androidSdkVersion": 35,
|
"androidSdkVersion": 36,
|
||||||
"hl": "en-GB",
|
"hl": "en-GB",
|
||||||
"gl": "US",
|
"gl": "US",
|
||||||
"utcOffsetMinutes": 0
|
"utcOffsetMinutes": 0
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
class TYTD.Queue
|
|
||||||
{
|
|
||||||
private ls = [];
|
|
||||||
private mtx = new Muxex();
|
|
||||||
|
|
||||||
public getCount()
|
|
||||||
{
|
|
||||||
this.mtx.Lock();
|
|
||||||
var c = this.ls.Count;
|
|
||||||
this.mtx.Unlock();
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Pop()
|
|
||||||
{
|
|
||||||
var item = null;
|
|
||||||
this.mtx.Lock();
|
|
||||||
if(this.ls.Count > 0)
|
|
||||||
{
|
|
||||||
item = ls[ls.Count-1];
|
|
||||||
this.ls.RemoveAt(ls.Count-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.mtx.Unlock();
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Push(val)
|
|
||||||
{
|
|
||||||
this.mtx.Lock();
|
|
||||||
this.ls.Add(val);
|
|
||||||
this.mtx.Unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,33 +4,21 @@ class Resolution
|
|||||||
static getNoDownload() "NoDownload";
|
static getNoDownload() "NoDownload";
|
||||||
/^ Get the video/audio mp4 (muxed by YouTube) ^/
|
/^ Get the video/audio mp4 (muxed by YouTube) ^/
|
||||||
static getLowVideo() "LowVideo";
|
static getLowVideo() "LowVideo";
|
||||||
/^ Get the highest video stream ^/
|
/^ Get the LowVideo stream (and convert to mp3) ^/
|
||||||
static getVideoOnly() "VideoOnly";
|
|
||||||
/^ Get the highest audio stream ^/
|
|
||||||
static getAudioOnly() "AudioOnly";
|
|
||||||
/^ Get the highest audio stream (and convert to mp3) ^/
|
|
||||||
static getMP3() "MP3";
|
static getMP3() "MP3";
|
||||||
/^ Get the highest audio stream (and convert to flac) ^/
|
/^ Get the LowVideo stream (and convert to flac) ^/
|
||||||
static getFLAC() "FLAC";
|
static getFLAC() "FLAC";
|
||||||
/^ Get the highest video and then audio stream (and convert to mp4) ^/
|
/^ Get the LowVideo stream (and convert to AMV for mp4 players like: https://www.ebay.com/sch/i.html?_nkw=AMV+mp3+player) ^/
|
||||||
static getMP4() "MP4";
|
static getAMV() "AMV";
|
||||||
/^ Get the highest video and then audio stream (and mux to a mkv file) ^/
|
|
||||||
static getMKV() "MKV";
|
|
||||||
/^ Get the highest video and then audio stream (dont convert or mux) ^/
|
|
||||||
static getDontConvert() "DontConvert";
|
|
||||||
|
|
||||||
static getResolutions()
|
static getResolutions()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
{name="Don't Download", value=Resolution.NoDownload},
|
{name="Don't Download", value=Resolution.NoDownload,filename=null},
|
||||||
{name="Low (muxed by YouTube)", value=Resolution.LowVideo,default=true},
|
{name="Low (muxed by YouTube)", value=Resolution.LowVideo,default=true,filename="ytmux.mp4"},
|
||||||
{name="Highest video (no audio)", value=Resolution.VideoOnly},
|
{name="Convert to MP3", value=Resolution.MP3,filename="conv.mp3"},
|
||||||
{name="Highest audio (no video)", value=Resolution.AudioOnly},
|
{name="Convert to FLAC", value=Resolution.FLAC,filename="conv.flac"},
|
||||||
{name="Convert to MP3", value=Resolution.MP3},
|
{name="Convert To AMV (for generic mp4 players)", value=Resolution.AMV,filename="conv.amv"}
|
||||||
{name="Convert to FLAC", value=Resolution.FLAC},
|
|
||||||
{name="Convert to MP4", value=Resolution.MP4},
|
|
||||||
{name="Mux to MKV (no transcoding)",value=Resolution.MKV},
|
|
||||||
{name="Don't convert or Mux",value=Resolution.DontConvert}
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
Tesses.YouTubeDownloader/src/error.tcross
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
class VideoDownloadError {
|
||||||
|
public VideoDownloadError(id, error)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Id;
|
||||||
|
|
||||||
|
public Error;
|
||||||
|
|
||||||
|
public ToString()
|
||||||
|
{
|
||||||
|
return $"Download error {Error} with Id: {Id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ func TYTD.GetVideoId(v)
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TYTD.GetPlaylistId(pid)
|
func TYTD.GetPlaylistId(pid)
|
||||||
{
|
{
|
||||||
func IsValidId(v)
|
func IsValidId(v)
|
||||||
@@ -84,4 +85,49 @@ func TYTD.GetChannelId(cid)
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TYTD.CreateYouTubeTempPlaylist(ids)
|
||||||
|
{
|
||||||
|
var url = "https://www.youtube.com/watch_videos?video_ids=";
|
||||||
|
var first=true;
|
||||||
|
each(var item : ids)
|
||||||
|
{
|
||||||
|
if(!first) url += $"{url},{Net.Http.UrlEncode(item)}";
|
||||||
|
else
|
||||||
|
url += Net.Http.UrlEncode(item);
|
||||||
|
first=false;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
func TYTD.GetYouTubeTempPlaylistRedirect(url)
|
||||||
|
{
|
||||||
|
if(url.Contains("/watch_videos?") && url.Contains("video_ids="))
|
||||||
|
{
|
||||||
|
var queryPart = url.Split("?",true,2);
|
||||||
|
return $"/watch_videos?{queryPart[1]}";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
func TYTD.GetYouTubeTempPlaylist(url)
|
||||||
|
{
|
||||||
|
if(url.Contains("/watch_videos?") && url.Contains("video_ids="))
|
||||||
|
{
|
||||||
|
var queryPart = url.Split("?",true,2);
|
||||||
|
if(queryPart.Length == 2)
|
||||||
|
{
|
||||||
|
var queryParms =queryPart[1].Split("&");
|
||||||
|
|
||||||
|
each(var item : queryParms)
|
||||||
|
{
|
||||||
|
const vals = item.Split("=",true,2);
|
||||||
|
if(vals.Length == 2 && vals[0] == "video_ids")
|
||||||
|
{
|
||||||
|
return Net.Http.UrlDecode(vals[1]).Split(",");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
class TYTD.AOVideoDownload : IVideoDownload {
|
|
||||||
private info;
|
|
||||||
private tytd;
|
|
||||||
private progress;
|
|
||||||
private video_stream_url;
|
|
||||||
|
|
||||||
private done=false;
|
|
||||||
|
|
||||||
public AOVideoDownload(id)
|
|
||||||
{
|
|
||||||
this.info = {
|
|
||||||
Title = "",
|
|
||||||
Channel = "",
|
|
||||||
VideoId = TYTD.GetVideoId(id),
|
|
||||||
ChannelId = ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public setTYTD(tytd)
|
|
||||||
{
|
|
||||||
this.tytd = tytd;
|
|
||||||
var id = this.info.VideoId;
|
|
||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
|
|
||||||
if(this.tytd.Storage.FileExists(path)) {
|
|
||||||
this.done = true;
|
|
||||||
return tytd;
|
|
||||||
}
|
|
||||||
|
|
||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
|
||||||
this.info.Title = req.videoDetails.title;
|
|
||||||
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Audio");
|
|
||||||
Console.WriteLine($"Downloading: {this.info.Title} with id: {id} Highest Audio");
|
|
||||||
this.info.Channel = req.videoDetails.author;
|
|
||||||
this.info.ChannelId = req.videoDetails.channelId;
|
|
||||||
|
|
||||||
this.tytd.PutVideoInfo(req.videoDetails);
|
|
||||||
|
|
||||||
var sampleRate = 0;
|
|
||||||
var bitrate = 0;
|
|
||||||
var url = "";
|
|
||||||
|
|
||||||
each(var item : req.streamingData.adaptiveFormats)
|
|
||||||
{
|
|
||||||
if(TypeOf(item.height) != "Long")
|
|
||||||
{
|
|
||||||
|
|
||||||
item.audioSampleRate = ParseLong(item.audioSampleRate);
|
|
||||||
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
|
|
||||||
{
|
|
||||||
url = item.url;
|
|
||||||
sampleRate = item.audioSampleRate;
|
|
||||||
bitrate = item.bitrate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video_stream_url = url;
|
|
||||||
|
|
||||||
return tytd;
|
|
||||||
}
|
|
||||||
public setProgress(p)
|
|
||||||
{
|
|
||||||
this.progress = p;
|
|
||||||
}
|
|
||||||
public getVideo()
|
|
||||||
{
|
|
||||||
return this.info;
|
|
||||||
}
|
|
||||||
public Start()
|
|
||||||
{
|
|
||||||
if(this.done) return;
|
|
||||||
for(var i = 0; i < 5; i++)
|
|
||||||
{
|
|
||||||
var req = {
|
|
||||||
FollowRedirects = true,
|
|
||||||
RequestHeaders = [
|
|
||||||
{
|
|
||||||
Key = "User-Agent",
|
|
||||||
|
|
||||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
var resp = Net.Http.MakeRequest(this.video_stream_url,req);
|
|
||||||
|
|
||||||
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
|
||||||
{
|
|
||||||
var id = this.info.VideoId;
|
|
||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
|
|
||||||
this.tytd.Storage.CreateDirectory(path.GetParent());
|
|
||||||
var strm = this.tytd.Storage.OpenFile(path+".part","wb");
|
|
||||||
var src = resp.ReadAsStream();
|
|
||||||
Helpers.CopyToProgress(src,strm,this.progress,100.0);
|
|
||||||
strm.Close();
|
|
||||||
src.Close();
|
|
||||||
this.tytd.Storage.MoveFile(path+".part",path);
|
|
||||||
break;
|
|
||||||
}else {
|
|
||||||
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
|
|
||||||
var sampleRate = 0;
|
|
||||||
var bitrate = 0;
|
|
||||||
var url = "";
|
|
||||||
|
|
||||||
each(var item : req.streamingData.adaptiveFormats)
|
|
||||||
{
|
|
||||||
if(TypeOf(item.height) != "Long")
|
|
||||||
{
|
|
||||||
|
|
||||||
item.audioSampleRate = ParseLong(item.audioSampleRate);
|
|
||||||
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
|
|
||||||
{
|
|
||||||
url = item.url;
|
|
||||||
sampleRate = item.audioSampleRate;
|
|
||||||
bitrate = item.bitrate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video_stream_url = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
class TYTD.NoConvertVideoDownload : IVideoDownload {
|
|
||||||
private info;
|
|
||||||
private tytd;
|
|
||||||
private progress;
|
|
||||||
private video_stream_url;
|
|
||||||
private audio_stream_url;
|
|
||||||
|
|
||||||
private done=false;
|
|
||||||
|
|
||||||
|
|
||||||
public NoConvertVideoDownload(id)
|
|
||||||
{
|
|
||||||
this.info = {
|
|
||||||
Title = "",
|
|
||||||
Channel = "",
|
|
||||||
VideoId = TYTD.GetVideoId(id),
|
|
||||||
ChannelId = ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public getVideo()
|
|
||||||
{
|
|
||||||
return this.info;
|
|
||||||
}
|
|
||||||
public setTYTD(tytd)
|
|
||||||
{
|
|
||||||
this.tytd = tytd;
|
|
||||||
var id = this.info.VideoId;
|
|
||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
|
|
||||||
var pathA = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
|
|
||||||
if(this.tytd.Storage.FileExists(path) && this.tytd.Storage.FileExists(pathA)) {
|
|
||||||
this.done = true;
|
|
||||||
return tytd;
|
|
||||||
}
|
|
||||||
|
|
||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
|
||||||
|
|
||||||
|
|
||||||
this.info.Title = req.videoDetails.title;
|
|
||||||
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video/Audio");
|
|
||||||
Console.WriteLine($"Downloading: {this.info.Title} with id: {id} Highest Video/Audio");
|
|
||||||
this.info.Channel = req.videoDetails.author;
|
|
||||||
this.info.ChannelId = req.videoDetails.channelId;
|
|
||||||
|
|
||||||
this.tytd.PutVideoInfo(req.videoDetails);
|
|
||||||
|
|
||||||
var width = 0;
|
|
||||||
var height = 0;
|
|
||||||
var sampleRate = 0;
|
|
||||||
var bitrate = 0;
|
|
||||||
var vurl = "";
|
|
||||||
var aurl = "";
|
|
||||||
|
|
||||||
each(var item : req.streamingData.adaptiveFormats)
|
|
||||||
{
|
|
||||||
if(TypeOf(item.height) == "Long")
|
|
||||||
{
|
|
||||||
if(item.width >= width && item.height >= height)
|
|
||||||
{
|
|
||||||
vurl = item.url;
|
|
||||||
width = item.width;
|
|
||||||
height = item.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
item.audioSampleRate = ParseLong(item.audioSampleRate);
|
|
||||||
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
|
|
||||||
{
|
|
||||||
aurl = item.url;
|
|
||||||
sampleRate = item.audioSampleRate;
|
|
||||||
bitrate = item.bitrate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video_stream_url = vurl;
|
|
||||||
this.audio_stream_url = aurl;
|
|
||||||
|
|
||||||
return tytd;
|
|
||||||
}
|
|
||||||
public setProgress(p)
|
|
||||||
{
|
|
||||||
this.progress = p;
|
|
||||||
}
|
|
||||||
public Start()
|
|
||||||
{
|
|
||||||
if(this.done) return;
|
|
||||||
|
|
||||||
var id = this.info.VideoId;
|
|
||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
|
|
||||||
var pathA = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
|
|
||||||
|
|
||||||
if(!this.tytd.Storage.FileExists(path))
|
|
||||||
{
|
|
||||||
for(var i = 0; i < 5; i++)
|
|
||||||
{
|
|
||||||
var req = {
|
|
||||||
FollowRedirects = true,
|
|
||||||
RequestHeaders = [
|
|
||||||
{
|
|
||||||
Key = "User-Agent",
|
|
||||||
|
|
||||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
var resp = Net.Http.MakeRequest(this.video_stream_url,req);
|
|
||||||
|
|
||||||
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
|
||||||
{
|
|
||||||
this.tytd.Storage.CreateDirectory(path.GetParent());
|
|
||||||
var strm = this.tytd.Storage.OpenFile(path+".part","wb");
|
|
||||||
var src = resp.ReadAsStream();
|
|
||||||
Helpers.CopyToProgress(src,strm,(p)=>{
|
|
||||||
this.progress(p/2);
|
|
||||||
},100.0);
|
|
||||||
strm.Close();
|
|
||||||
src.Close();
|
|
||||||
this.tytd.Storage.MoveFile(path+".part",path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
|
|
||||||
var width = 0;
|
|
||||||
var height = 0;
|
|
||||||
var sampleRate = 0;
|
|
||||||
var bitrate = 0;
|
|
||||||
var vurl = "";
|
|
||||||
var aurl = "";
|
|
||||||
|
|
||||||
each(var item : req.streamingData.adaptiveFormats)
|
|
||||||
{
|
|
||||||
if(TypeOf(item.height) == "Long")
|
|
||||||
{
|
|
||||||
if(item.width >= width && item.height >= height)
|
|
||||||
{
|
|
||||||
vurl = item.url;
|
|
||||||
width = item.width;
|
|
||||||
height = item.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
item.audioSampleRate = ParseLong(item.audioSampleRate);
|
|
||||||
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
|
|
||||||
{
|
|
||||||
aurl = item.url;
|
|
||||||
sampleRate = item.audioSampleRate;
|
|
||||||
bitrate = item.bitrate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video_stream_url = vurl;
|
|
||||||
this.audio_stream_url = aurl;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!this.tytd.Storage.FileExists(pathA))
|
|
||||||
{
|
|
||||||
for(var i = 0; i < 5; i++)
|
|
||||||
{
|
|
||||||
var req = {
|
|
||||||
FollowRedirects = true,
|
|
||||||
RequestHeaders = [
|
|
||||||
{
|
|
||||||
Key = "User-Agent",
|
|
||||||
|
|
||||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
var resp = Net.Http.MakeRequest(this.audio_stream_url,req);
|
|
||||||
|
|
||||||
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
|
||||||
{
|
|
||||||
this.tytd.Storage.CreateDirectory(pathA.GetParent());
|
|
||||||
var strm = this.tytd.Storage.OpenFile(pathA+".part","wb");
|
|
||||||
var src = resp.ReadAsStream();
|
|
||||||
Helpers.CopyToProgress(src,strm,(p)=>{
|
|
||||||
this.progress((p/2)+0.5);
|
|
||||||
},100.0);
|
|
||||||
strm.Close();
|
|
||||||
src.Close();
|
|
||||||
this.tytd.Storage.MoveFile(pathA+".part",pathA);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
|
|
||||||
var width = 0;
|
|
||||||
var height = 0;
|
|
||||||
var sampleRate = 0;
|
|
||||||
var bitrate = 0;
|
|
||||||
var vurl = "";
|
|
||||||
var aurl = "";
|
|
||||||
|
|
||||||
each(var item : req.streamingData.adaptiveFormats)
|
|
||||||
{
|
|
||||||
if(TypeOf(item.height) == "Long")
|
|
||||||
{
|
|
||||||
if(item.width >= width && item.height >= height)
|
|
||||||
{
|
|
||||||
vurl = item.url;
|
|
||||||
width = item.width;
|
|
||||||
height = item.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
item.audioSampleRate = ParseLong(item.audioSampleRate);
|
|
||||||
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
|
|
||||||
{
|
|
||||||
aurl = item.url;
|
|
||||||
sampleRate = item.audioSampleRate;
|
|
||||||
bitrate = item.bitrate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video_stream_url = vurl;
|
|
||||||
this.audio_stream_url = aurl;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.progress(1.0);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,9 +20,17 @@ class TYTD.SDVideoDownload : IVideoDownload {
|
|||||||
this.tytd = tytd;
|
this.tytd = tytd;
|
||||||
var id = this.info.VideoId;
|
var id = this.info.VideoId;
|
||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ytmux.mp4";
|
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ytmux.mp4";
|
||||||
if(this.tytd.Storage.FileExists(path)) {
|
if(this.tytd.Storage.FileExists(path)) {
|
||||||
this.done = true;
|
this.done = true;
|
||||||
return tytd;
|
const vinfo = this.tytd.GetVideo(id);
|
||||||
|
this.info = {
|
||||||
|
Title = vinfo.title,
|
||||||
|
VideoId = vinfo.videoId,
|
||||||
|
ChannelId = vinfo.channelId,
|
||||||
|
Channel = vinfo.author
|
||||||
|
};
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
var req = this.tytd.ManifestRequest(id).playerResponse;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
class TYTD.TranscodeAMV : IVideoDownload {
|
||||||
|
private id;
|
||||||
|
private ncv;
|
||||||
|
private tytd;
|
||||||
|
private done;
|
||||||
|
|
||||||
|
public TranscodeAMV(id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.ncv = new TYTD.SDVideoDownload(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTYTD(tytd)
|
||||||
|
{
|
||||||
|
this.tytd = tytd;
|
||||||
|
this.ncv.TYTD = tytd;
|
||||||
|
|
||||||
|
if(TypeIsDefined(tytd))
|
||||||
|
{
|
||||||
|
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
|
||||||
|
var ao = dir / "ytmux.mp4";
|
||||||
|
var out = dir /"conv.amv";
|
||||||
|
|
||||||
|
if(FS.Local.FileExists(out)) return null;
|
||||||
|
return tytd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setProgress(p)
|
||||||
|
{
|
||||||
|
return this.ncv.Progress = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVideo()
|
||||||
|
{
|
||||||
|
return this.ncv.Video;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Start()
|
||||||
|
{
|
||||||
|
var id = this.id;
|
||||||
|
this.ncv.Start();
|
||||||
|
|
||||||
|
var p = new Process();
|
||||||
|
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
||||||
|
|
||||||
|
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
|
||||||
|
var ao = dir / "ytmux.mp4";
|
||||||
|
var out = dir / $"conv.amv";
|
||||||
|
if(FS.Local.FileExists(out) || !FS.Local.FileExists(ao)) return;
|
||||||
|
var args=["-y","-i",ao.ToString()];
|
||||||
|
//-c:v amv -c:a adpcm_ima_amv -pix_fmt yuvj420p -vstrict -1 -s 160x120 -ac 1 -ar 22050 -r 16 -block_size 882
|
||||||
|
|
||||||
|
|
||||||
|
args.Add("-c:v");
|
||||||
|
args.Add("amv");
|
||||||
|
args.Add("-c:a");
|
||||||
|
args.Add("adpcm_ima_amv");
|
||||||
|
args.Add("-pix_fmt");
|
||||||
|
args.Add("yuvj420p");
|
||||||
|
args.Add("-vstrict");
|
||||||
|
args.Add("-1");
|
||||||
|
args.Add("-s");
|
||||||
|
args.Add("160x120");
|
||||||
|
args.Add("-ac");
|
||||||
|
args.Add("1");
|
||||||
|
args.Add("-ar");
|
||||||
|
args.Add("22050");
|
||||||
|
args.Add("-r");
|
||||||
|
args.Add("15");
|
||||||
|
args.Add("-block_size");
|
||||||
|
args.Add("1470");
|
||||||
|
args.Add("-preset");
|
||||||
|
args.Add("ultrafast");
|
||||||
|
args.Add(out.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
p.Arguments = args;
|
||||||
|
if(p.Start())
|
||||||
|
p.Join();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,24 @@ class TYTD.TranscodeAudio : IVideoDownload {
|
|||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.ext = ext;
|
this.ext = ext;
|
||||||
this.ncv = new TYTD.AOVideoDownload(id);
|
this.ncv = new TYTD.SDVideoDownload(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setTYTD(tytd)
|
public setTYTD(tytd)
|
||||||
{
|
{
|
||||||
this.tytd = tytd;
|
this.tytd = tytd;
|
||||||
return this.ncv.TYTD = tytd;
|
this.ncv.TYTD = tytd;
|
||||||
|
|
||||||
|
if(TypeIsDefined(tytd))
|
||||||
|
{
|
||||||
|
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
|
||||||
|
var out = dir /"conv.{this.ext}";
|
||||||
|
|
||||||
|
if(FS.Local.FileExists(out)) return null;
|
||||||
|
return tytd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setProgress(p)
|
public setProgress(p)
|
||||||
@@ -31,15 +42,15 @@ class TYTD.TranscodeAudio : IVideoDownload {
|
|||||||
public Start()
|
public Start()
|
||||||
{
|
{
|
||||||
var id = this.id;
|
var id = this.id;
|
||||||
this.mcv.Start();
|
this.ncv.Start();
|
||||||
|
|
||||||
var p = new Process();
|
var p = new Process();
|
||||||
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
||||||
|
|
||||||
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
|
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
|
||||||
var ao = dir / "ao.bin";
|
var ao = dir / "ytmux.mp4";
|
||||||
var out = dir / $"conv{this.ext}";
|
var out = dir / $"conv{this.ext}";
|
||||||
if(FS.Local.FileExists(out)) return;
|
if(FS.Local.FileExists(out) && !FS.Local.FileExists(ao)) return;
|
||||||
var args=["-y","-i",ao.ToString()];
|
var args=["-y","-i",ao.ToString()];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
class TYTD.TranscodeVideo : IVideoDownload {
|
|
||||||
private id;
|
|
||||||
private ncv;
|
|
||||||
private tytd;
|
|
||||||
private ext;
|
|
||||||
|
|
||||||
public TranscodeVideo(id,ext)
|
|
||||||
{
|
|
||||||
this.id = id;
|
|
||||||
this.ext = ext;
|
|
||||||
this.ncv = new TYTD.NoConvertVideoDownload(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setTYTD(tytd)
|
|
||||||
{
|
|
||||||
this.tytd = tytd;
|
|
||||||
return this.ncv.TYTD = tytd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setProgress(p)
|
|
||||||
{
|
|
||||||
return this.ncv.Progress = p;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getVideo()
|
|
||||||
{
|
|
||||||
return this.ncv.Video;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Start()
|
|
||||||
{
|
|
||||||
var id = this.id;
|
|
||||||
this.mcv.Start();
|
|
||||||
|
|
||||||
var p = new Process();
|
|
||||||
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
|
||||||
|
|
||||||
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
|
|
||||||
var vo = dir / "vo.bin";
|
|
||||||
var ao = dir / "ao.bin";
|
|
||||||
var out = dir / $"conv{this.ext}";
|
|
||||||
if(FS.Local.FileExists(out)) return;
|
|
||||||
var args=["-y","-i",vo.ToString(),"-i",ao.ToString()];
|
|
||||||
|
|
||||||
if(this.ext == ".mkv")
|
|
||||||
{
|
|
||||||
args.Add("-c");
|
|
||||||
args.Add("copy");
|
|
||||||
}
|
|
||||||
args.Add("-map");
|
|
||||||
args.Add("0:v");
|
|
||||||
args.Add("-map");
|
|
||||||
args.Add("1:a");
|
|
||||||
args.Add("-preset");
|
|
||||||
args.Add("ultrafast");
|
|
||||||
args.Add(out.ToString());
|
|
||||||
|
|
||||||
|
|
||||||
p.Arguments = args;
|
|
||||||
if(p.Start())
|
|
||||||
p.Join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
class TYTD.VOVideoDownload : IVideoDownload {
|
|
||||||
private info;
|
|
||||||
private tytd;
|
|
||||||
private progress;
|
|
||||||
private video_stream_url;
|
|
||||||
|
|
||||||
private done=false;
|
|
||||||
|
|
||||||
public VOVideoDownload(id)
|
|
||||||
{
|
|
||||||
this.info = {
|
|
||||||
Title = "",
|
|
||||||
Channel = "",
|
|
||||||
VideoId = TYTD.GetVideoId(id),
|
|
||||||
ChannelId = ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public setTYTD(tytd)
|
|
||||||
{
|
|
||||||
this.tytd = tytd;
|
|
||||||
var id = this.info.VideoId;
|
|
||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
|
|
||||||
if(this.tytd.Storage.FileExists(path)) {
|
|
||||||
this.done = true;
|
|
||||||
return tytd;
|
|
||||||
}
|
|
||||||
|
|
||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
|
||||||
this.info.Title = req.videoDetails.title;
|
|
||||||
this.info.Channel = req.videoDetails.author;
|
|
||||||
this.info.ChannelId = req.videoDetails.channelId;
|
|
||||||
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video");
|
|
||||||
Console.WriteLine($"Downloading: {this.info.Title} with id: {id} Highest Video");
|
|
||||||
|
|
||||||
this.tytd.PutVideoInfo(req.videoDetails);
|
|
||||||
|
|
||||||
var width = 0;
|
|
||||||
var height = 0;
|
|
||||||
var url = "";
|
|
||||||
|
|
||||||
each(var item : req.streamingData.adaptiveFormats)
|
|
||||||
{
|
|
||||||
if(TypeOf(item.height) == "Long")
|
|
||||||
{
|
|
||||||
if(item.width >= width && item.height >= height)
|
|
||||||
{
|
|
||||||
url = item.url;
|
|
||||||
width = item.width;
|
|
||||||
height = item.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video_stream_url = url;
|
|
||||||
|
|
||||||
return tytd;
|
|
||||||
}
|
|
||||||
public setProgress(p)
|
|
||||||
{
|
|
||||||
this.progress = p;
|
|
||||||
}
|
|
||||||
public getVideo()
|
|
||||||
{
|
|
||||||
return this.info;
|
|
||||||
}
|
|
||||||
public Start()
|
|
||||||
{
|
|
||||||
if(this.done) return;
|
|
||||||
var req = {
|
|
||||||
FollowRedirects = true,
|
|
||||||
RequestHeaders = [
|
|
||||||
{
|
|
||||||
Key = "User-Agent",
|
|
||||||
|
|
||||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
var resp = Net.Http.MakeRequest(this.video_stream_url,req);
|
|
||||||
|
|
||||||
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
|
||||||
{
|
|
||||||
var id = this.info.VideoId;
|
|
||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
|
|
||||||
this.tytd.Storage.CreateDirectory(path.GetParent());
|
|
||||||
var strm = this.tytd.Storage.OpenFile(path+".part","wb");
|
|
||||||
var src = resp.ReadAsStream();
|
|
||||||
Helpers.CopyToProgress(src,strm,this.progress,100.0);
|
|
||||||
strm.Close();
|
|
||||||
src.Close();
|
|
||||||
this.tytd.Storage.MoveFile(path+".part",path);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
|
|
||||||
var width = 0;
|
|
||||||
var height = 0;
|
|
||||||
var url = "";
|
|
||||||
|
|
||||||
each(var item : req.streamingData.adaptiveFormats)
|
|
||||||
{
|
|
||||||
if(TypeOf(item.height) == "Long")
|
|
||||||
{
|
|
||||||
if(item.width >= width && item.height >= height)
|
|
||||||
{
|
|
||||||
url = item.url;
|
|
||||||
width = item.width;
|
|
||||||
height = item.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video_stream_url = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
build-for-system-pkg.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
cd Tesses.YouTubeDownloader.Server
|
||||||
|
crosslang build
|
||||||
|
cd bin
|
||||||
|
cp Tesses.YouTubeDownloader.Server-*.crvm tytd2025.crvm
|
||||||
|
cd ../..
|
||||||
|
mkdir -p packages-tmp
|
||||||
|
nfpm package -t packages-tmp -p archlinux
|
||||||
|
curl --user tesses50:$PACKAGE_AND_BREW \
|
||||||
|
--upload-file packages-tmp/*.pkg.tar.zst \
|
||||||
|
https://git.tesses.org/api/packages/tesses50/arch/core
|
||||||
|
nfpm package -t packages-tmp -p deb
|
||||||
|
curl --user tesses50:$PACKAGE_AND_BREW \
|
||||||
|
--upload-file packages-tmp/*.deb \
|
||||||
|
https://git.tesses.org/api/packages/tesses50/debian/pool/universal/main/upload
|
||||||
|
nfpm package -t packages-tmp -p rpm
|
||||||
|
curl --user tesses50:$PACKAGE_AND_BREW \
|
||||||
|
--upload-file packages-tmp/*.rpm \
|
||||||
|
https://git.tesses.org/api/packages/tesses50/rpm/upload
|
||||||
7
changeme.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 1.0.1
|
||||||
|
Target crosslang v0.0.10, add /api/v1/progress_ex.json
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
Make it a little faster, queue is in sqlite now, fix db bug and more
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
tytd:
|
tytd:
|
||||||
image: onedev.site.tesses.net/tytd2025/tytd2025:latest
|
image: git.tesses.org/tesses50/tytd2025:latest
|
||||||
volumes:
|
volumes:
|
||||||
- tytd-data:/data
|
- tytd-data:/data
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
43
nfpm.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# This is an example nfpm configuration file.
|
||||||
|
# Make sure to check the documentation at https://nfpm.goreleaser.com
|
||||||
|
#
|
||||||
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
# Feel free to remove those if you don't want/need to use them.
|
||||||
|
# yaml-language-server: $schema=https://nfpm.goreleaser.com/schema.json
|
||||||
|
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||||
|
|
||||||
|
name: "tytd2025"
|
||||||
|
arch: "any"
|
||||||
|
platform: "linux"
|
||||||
|
version: "1.0.1"
|
||||||
|
section: "default"
|
||||||
|
priority: "extra"
|
||||||
|
|
||||||
|
depends:
|
||||||
|
- crosslang
|
||||||
|
recommends:
|
||||||
|
- ffmpeg
|
||||||
|
maintainer: "Mike Nolan <tesses@tesses.net>"
|
||||||
|
description: Tesses YouTube Downloader
|
||||||
|
vendor: "Tesses"
|
||||||
|
homepage: "https://git.tesses.org/tesses50/tytd2025.git"
|
||||||
|
license: "GPLv3"
|
||||||
|
contents:
|
||||||
|
- src: "Tesses.YouTubeDownloader.Server/bin/tytd2025.crvm"
|
||||||
|
dst: "/opt/tytd2025/tytd2025.crvm"
|
||||||
|
- src: "Tesses.YouTubeDownloader.Server/bin/Tesses.YouTubeDownloader-*.crvm"
|
||||||
|
dst: "/opt/tytd2025/"
|
||||||
|
- src: "Tesses.YouTubeDownloader.Server/bin/Tesses.CrossLang.BuildEssentials-*.crvm"
|
||||||
|
dst: "/opt/tytd2025/"
|
||||||
|
- src: package/systemd/
|
||||||
|
dst: /etc/systemd/system/
|
||||||
|
type: tree
|
||||||
|
- src: package/usrbin/
|
||||||
|
dst: /usr/bin/
|
||||||
|
type: tree
|
||||||
|
file_info:
|
||||||
|
mode: 0755
|
||||||
|
|
||||||
|
scripts:
|
||||||
|
postinstall: ./package/postinstall.sh
|
||||||
|
preremove: ./package/preremove.sh
|
||||||
5
package/postinstall.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
useradd --system --create-home --shell /usr/sbin/nologin tytd2025 || true
|
||||||
|
|
||||||
|
if command -v deb-systemd-helper >/dev/null 2>&1; then
|
||||||
|
deb-systemd-helper enable tytd2025
|
||||||
|
fi
|
||||||
3
package/preremove.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
if command -v deb-systemd-helper >/dev/null 2>&1; then
|
||||||
|
deb-systemd-helper disable tytd2025
|
||||||
|
fi
|
||||||
14
package/systemd/tytd2025.service
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tesses YouTube Downloader
|
||||||
|
Documentation=https://git.tesses.org/tesses50/tytd2025
|
||||||
|
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/tytd2025
|
||||||
|
Restart=on-failure
|
||||||
|
User=tytd2025
|
||||||
|
StandardOutput=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
2
package/usrbin/tytd2025
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
exec crosslang vm "/opt/tytd2025/tytd2025.crvm" --port=3255
|
||||||