Compare commits
12 Commits
8007eeb043
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 ["crossvm","/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 writen 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.tesseslangauge.com/) or any CPKG compliant server
|
||||||
|
- Can download YouTube videos either Low quality (but doesn't) require [ffmpeg](https://ffmpeg.org/), you can also download individual streams (also doesn't need [ffmpeg](https://ffmpeg.org/)), or to MP4 (doesn't work on wii due to libx264 having illegal instruction), MKV (so no transcode), MP3 or FLAC (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://onedev.site.tesses.net/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.0.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.0.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 vid = tytd.CurrentVideo;
|
||||||
|
|
||||||
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(vid.VideoId)}"} href={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"}>{vid.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(vid.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"}>{vid.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={tytd.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({
|
||||||
@@ -70,14 +141,17 @@ class TYTDApp {
|
|||||||
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
||||||
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
|
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
|
||||||
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
const password = ctx.QueryParams.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;
|
||||||
@@ -151,7 +248,7 @@ class TYTDApp {
|
|||||||
case 3:
|
case 3:
|
||||||
{
|
{
|
||||||
|
|
||||||
const createUser = ctx.QueryParams.GetFirstBoolean("user");
|
const createUser = (ctx.QueryParams.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,11 +341,49 @@ 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 incorrect=false;
|
||||||
@@ -265,6 +399,7 @@ class TYTDApp {
|
|||||||
|
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
|
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(ctx.Path == "/newuser")
|
else if(ctx.Path == "/newuser")
|
||||||
{
|
{
|
||||||
var error = null;
|
var error = null;
|
||||||
@@ -340,6 +475,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");
|
||||||
@@ -371,6 +536,20 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("application/json").SendJson(jo);
|
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 +579,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 +629,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")
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
@@ -579,6 +797,24 @@ class TYTDApp {
|
|||||||
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 +835,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");
|
||||||
@@ -677,7 +913,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")
|
||||||
@@ -714,6 +950,36 @@ 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")
|
||||||
|
{
|
||||||
|
const video_ids = ctx.QueryParams.TryGetFirst("video_ids");
|
||||||
|
if(TypeIsString(video_ids))
|
||||||
|
{
|
||||||
|
if(ctx.Method=="GET")
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.YouTubeAnonyPlaylist(video_ids));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(ctx.Method=="POST")
|
||||||
|
{
|
||||||
|
const name = ctx.QueryParams.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));
|
||||||
@@ -802,6 +1068,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));
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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}
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,8 @@ class UserFlags {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
static getITTR() 35000;
|
static getITTR() 35000;
|
||||||
|
|
||||||
|
static getExpires() 86400 * 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TYTD.Downloader {
|
class TYTD.Downloader {
|
||||||
@@ -65,6 +67,50 @@ class TYTD.Downloader {
|
|||||||
this.Storage = vfs;
|
this.Storage = vfs;
|
||||||
this.DatabaseDirectory = dbDir;
|
this.DatabaseDirectory = dbDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PopQueue()
|
||||||
|
{
|
||||||
|
Mutex.Lock();
|
||||||
|
const db = OpenDB();
|
||||||
|
var res = Sqlite.Exec(db,"SELECT * FROM queue ORDER BY id DESC LIMIT 1;");
|
||||||
|
|
||||||
|
if(TypeIsList(res) && res.Length > 0)
|
||||||
|
{
|
||||||
|
Sqlite.Exec(db,$"DELETE FROM queue WHERE id = {Sqlite.Escape(res[0].id)};");
|
||||||
|
res = res[0];
|
||||||
|
}
|
||||||
|
else { res = null;}
|
||||||
|
Sqlite.Close(db);
|
||||||
|
Mutex.Unlock();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(TypeIsDictionary(res))
|
||||||
|
{
|
||||||
|
switch(res.resolution)
|
||||||
|
{
|
||||||
|
case Resolution.NoDownload:
|
||||||
|
{
|
||||||
|
this.PutVideoInfoIfNotExists(res.videoId);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Resolution.MP3:
|
||||||
|
return new TYTD.TranscodeAudio(res.videoId,".mp3");
|
||||||
|
case Resolution.FLAC:
|
||||||
|
return new TYTD.TranscodeAudio(res.videoId,".flac");
|
||||||
|
case Resolution.AMV:
|
||||||
|
return new TYTD.TranscodeAMV(res.videoId);
|
||||||
|
default:
|
||||||
|
return new TYTD.SDVideoDownload(res.videoId);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/^
|
/^
|
||||||
Download a video
|
Download a video
|
||||||
id: video id or url
|
id: video id or url
|
||||||
@@ -72,42 +118,37 @@ class TYTD.Downloader {
|
|||||||
^/
|
^/
|
||||||
public DownloadVideo(id,$res)
|
public DownloadVideo(id,$res)
|
||||||
{
|
{
|
||||||
this.LOG($"Adding video: {TYTD.GetVideoId(id)}, original val: {id}");
|
const theVideoId = TYTD.GetVideoId(id);
|
||||||
switch(res)
|
if(TypeIsString(theVideoId))
|
||||||
{
|
{
|
||||||
case Resolution.NoDownload:
|
|
||||||
{
|
if(!TypeIsDefined(res)) res = Resolution.LowVideo;
|
||||||
var id = TYTD.GetVideoId(id);
|
const ent = {
|
||||||
if(id != null)
|
Id = theVideoId,
|
||||||
PutVideoInfoIfNotExists(id);
|
Resolution = res,
|
||||||
|
Cancel = false,
|
||||||
|
Type = "Video"
|
||||||
|
};
|
||||||
|
this.BeforeQueued.Invoke(this, ent);
|
||||||
|
if(ent.Cancel) {
|
||||||
|
this.LOG("Adding video canceled: {theVideoId}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case Resolution.LowVideo:
|
|
||||||
this.Queue.Push(new TYTD.SDVideoDownload(id));
|
|
||||||
break;
|
|
||||||
case Resolution.VideoOnly:
|
|
||||||
this.Queue.Push(new TYTD.VOVideoDownload(id));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Resolution.AudioOnly:
|
|
||||||
this.Queue.Push(new TYTD.AOVideoDownload(id));
|
|
||||||
break;
|
|
||||||
case Resolution.MP4:
|
|
||||||
this.Queue.Push(new TYTD.TranscodeVideo(id,".mp4"));
|
|
||||||
break;
|
|
||||||
case Resolution.MKV:
|
|
||||||
this.Queue.Push(new TYTD.TranscodeVideo(id,".mkv"));
|
|
||||||
break;
|
|
||||||
case Resolution.MP3:
|
|
||||||
this.Queue.Push(new TYTD.TranscodeAudio(id,".mp3"));
|
|
||||||
break;
|
|
||||||
case Resolution.FLAC:
|
|
||||||
this.Queue.Push(new TYTD.TranscodeAudio(id,".flac"));
|
|
||||||
break;
|
|
||||||
case Resolution.DontConvert:
|
|
||||||
this.Queue.Push(new TYTD.NoConvertVideoDownload(id));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
this.LOG($"Malformed video id {id}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.LOG($"Adding video: https://www.youtube.com/watch?v={theVideoId}");
|
||||||
|
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT, resolution TEXT);");
|
||||||
|
Mutex.Lock();
|
||||||
|
const db = OpenDB();
|
||||||
|
Sqlite.Exec(db, $"INSERT INTO queue (videoId, resolution) VALUES ({Sqlite.Escape(theVideoId)},{Sqlite.Escape(res)});");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
Mutex.Unlock();
|
||||||
|
|
||||||
}
|
}
|
||||||
/^
|
/^
|
||||||
Download a playlist
|
Download a playlist
|
||||||
@@ -121,6 +162,19 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
if(pid != null)
|
if(pid != null)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
const ent = {
|
||||||
|
Id = pid,
|
||||||
|
Resolution = res,
|
||||||
|
Cancel = false,
|
||||||
|
Type = "Playlist"
|
||||||
|
};
|
||||||
|
this.BeforeQueued.Invoke(this, ent);
|
||||||
|
if(ent.Cancel) {
|
||||||
|
this.LOG("Adding playlist canceled: {pid}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.LOG($"Adding playlist: https://www.youtube.com/playlist?list={pid}");
|
this.LOG($"Adding playlist: https://www.youtube.com/playlist?list={pid}");
|
||||||
this.PlaylistQueue.Push(()=>{
|
this.PlaylistQueue.Push(()=>{
|
||||||
each(var item : this.QueryPlaylistItems(pid,true))
|
each(var item : this.QueryPlaylistItems(pid,true))
|
||||||
@@ -145,6 +199,18 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
if(cid != null)
|
if(cid != null)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
const ent = {
|
||||||
|
Id = cid,
|
||||||
|
Resolution = res,
|
||||||
|
Cancel = false,
|
||||||
|
Type = "Channel"
|
||||||
|
};
|
||||||
|
this.BeforeQueued.Invoke(this, ent);
|
||||||
|
if(ent.Cancel) {
|
||||||
|
this.LOG("Adding channel canceled: {cid}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.LOG($"Adding channel: https://www.youtube.com/channel/{cid}");
|
this.LOG($"Adding channel: https://www.youtube.com/channel/{cid}");
|
||||||
this.PlaylistQueue.Push(()=>{
|
this.PlaylistQueue.Push(()=>{
|
||||||
each(var item : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
|
each(var item : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
|
||||||
@@ -182,6 +248,7 @@ class TYTD.Downloader {
|
|||||||
var pid = TYTD.GetPlaylistId(url);
|
var pid = TYTD.GetPlaylistId(url);
|
||||||
|
|
||||||
var cid = TYTD.GetChannelId(url);
|
var cid = TYTD.GetChannelId(url);
|
||||||
|
var tmp = TYTD.GetYouTubeTempPlaylist(url);
|
||||||
|
|
||||||
if(vid != null)
|
if(vid != null)
|
||||||
{
|
{
|
||||||
@@ -195,7 +262,13 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
this.DownloadChannel(cid,res);
|
this.DownloadChannel(cid,res);
|
||||||
}
|
}
|
||||||
|
else if(tmp != null)
|
||||||
|
{
|
||||||
|
each(var item : tmp)
|
||||||
|
{
|
||||||
|
this.DownloadItem(item,res);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/^
|
/^
|
||||||
Redirect url to info page
|
Redirect url to info page
|
||||||
@@ -204,6 +277,8 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
var vid = TYTD.GetVideoId(url);
|
var vid = TYTD.GetVideoId(url);
|
||||||
var pid = TYTD.GetPlaylistId(url);
|
var pid = TYTD.GetPlaylistId(url);
|
||||||
|
var cid = TYTD.GetChannelId(url);
|
||||||
|
var tmp = TYTD.GetYouTubeTempPlaylistRedirect(url);
|
||||||
|
|
||||||
if(vid != null)
|
if(vid != null)
|
||||||
{
|
{
|
||||||
@@ -217,6 +292,10 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
return $"./channel?id={Net.Http.UrlEncode(cid)}";
|
return $"./channel?id={Net.Http.UrlEncode(cid)}";
|
||||||
}
|
}
|
||||||
|
else if(tmp != null)
|
||||||
|
{
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
return "./";
|
return "./";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +310,18 @@ class TYTD.Downloader {
|
|||||||
if(views < 1000000000000) return $"{views/1000000000}B views";
|
if(views < 1000000000000) return $"{views/1000000000}B views";
|
||||||
return $"{views/1000000000000}T views";
|
return $"{views/1000000000000}T views";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GetQueueItems(offset, count)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
var db = this.OpenDB();
|
||||||
|
const vals = Sqlite.Exec(db,$"SELECT * FROM queue ORDER BY id DESC LIMIT {count} OFFSET {offset*count};");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
if(TypeIsList(vals))
|
||||||
|
return vals;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/^
|
/^
|
||||||
Get videos
|
Get videos
|
||||||
@@ -239,7 +330,7 @@ class TYTD.Downloader {
|
|||||||
^/
|
^/
|
||||||
public GetVideos(query, offset, count)
|
public GetVideos(query, offset, count)
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
|
|
||||||
var q = Sqlite.Escape($"%{query}%");
|
var q = Sqlite.Escape($"%{query}%");
|
||||||
@@ -274,7 +365,7 @@ class TYTD.Downloader {
|
|||||||
^/
|
^/
|
||||||
public GetPlaylists(query, offset, count)
|
public GetPlaylists(query, offset, count)
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var q = Sqlite.Escape($"%{query}%");
|
var q = Sqlite.Escape($"%{query}%");
|
||||||
var res = Sqlite.Exec(db, $"SELECT * FROM playlists v WHERE (v.title LIKE {q}) LIMIT {count} OFFSET {offset*count};");
|
var res = Sqlite.Exec(db, $"SELECT * FROM playlists v WHERE (v.title LIKE {q}) LIMIT {count} OFFSET {offset*count};");
|
||||||
@@ -300,7 +391,7 @@ class TYTD.Downloader {
|
|||||||
channelTitle="N/A",
|
channelTitle="N/A",
|
||||||
items = []
|
items = []
|
||||||
};
|
};
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var channelTitle = "";
|
var channelTitle = "";
|
||||||
var title = "";
|
var title = "";
|
||||||
@@ -364,7 +455,7 @@ class TYTD.Downloader {
|
|||||||
authorName = "N/A",
|
authorName = "N/A",
|
||||||
items = []
|
items = []
|
||||||
};
|
};
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var authorName = "";
|
var authorName = "";
|
||||||
|
|
||||||
@@ -408,7 +499,7 @@ class TYTD.Downloader {
|
|||||||
^/
|
^/
|
||||||
public GetChannels(query, offset, count)
|
public GetChannels(query, offset, count)
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var q = Sqlite.Escape($"%{query}%");
|
var q = Sqlite.Escape($"%{query}%");
|
||||||
var res = Sqlite.Exec(db, $"SELECT * FROM channels v WHERE (v.title LIKE {q}) LIMIT {count} OFFSET {offset*count};");
|
var res = Sqlite.Exec(db, $"SELECT * FROM channels v WHERE (v.title LIKE {q}) LIMIT {count} OFFSET {offset*count};");
|
||||||
@@ -468,7 +559,7 @@ class TYTD.Downloader {
|
|||||||
var id = TYTD.GetVideoId(vid);
|
var id = TYTD.GetVideoId(vid);
|
||||||
|
|
||||||
if(id == null) return null;
|
if(id == null) return null;
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var res = Sqlite.Exec(db, $"SELECT * FROM videos WHERE videoId = {Sqlite.Escape(id)};");
|
var res = Sqlite.Exec(db, $"SELECT * FROM videos WHERE videoId = {Sqlite.Escape(id)};");
|
||||||
|
|
||||||
@@ -504,7 +595,7 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
var id = TYTD.GetPlaylistId(vid);
|
var id = TYTD.GetPlaylistId(vid);
|
||||||
if(id == null) return null;
|
if(id == null) return null;
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var res = Sqlite.Exec(db, $"SELECT * FROM playlists WHERE playlistId = {Sqlite.Escape(id)};");
|
var res = Sqlite.Exec(db, $"SELECT * FROM playlists WHERE playlistId = {Sqlite.Escape(id)};");
|
||||||
|
|
||||||
@@ -526,7 +617,7 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
var id = TYTD.GetChannelId(vid);
|
var id = TYTD.GetChannelId(vid);
|
||||||
if(id == null) return null;
|
if(id == null) return null;
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var res = Sqlite.Exec(db, $"SELECT * FROM channels WHERE channelId = {Sqlite.Escape(id)};");
|
var res = Sqlite.Exec(db, $"SELECT * FROM channels WHERE channelId = {Sqlite.Escape(id)};");
|
||||||
|
|
||||||
@@ -543,7 +634,7 @@ class TYTD.Downloader {
|
|||||||
/^ Get the list of personal list names^/
|
/^ Get the list of personal list names^/
|
||||||
public GetPersonalLists()
|
public GetPersonalLists()
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@@ -565,7 +656,7 @@ class TYTD.Downloader {
|
|||||||
/^ Set the description of a personal list ^/
|
/^ Set the description of a personal list ^/
|
||||||
public SetPersonalListDescription(name,description)
|
public SetPersonalListDescription(name,description)
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
|
|
||||||
Sqlite.Exec(db, $"UPDATE personal_lists SET description = {Sqlite.Escape(description)} WHERE name = {Sqlite.Escape(name)};");
|
Sqlite.Exec(db, $"UPDATE personal_lists SET description = {Sqlite.Escape(description)} WHERE name = {Sqlite.Escape(name)};");
|
||||||
@@ -575,7 +666,7 @@ class TYTD.Downloader {
|
|||||||
/^ Get the description of a personal list ^/
|
/^ Get the description of a personal list ^/
|
||||||
public GetPersonalListDescription(name)
|
public GetPersonalListDescription(name)
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
|
|
||||||
var res = Sqlite.Exec(db, $"SELECT * FROM personal_lists WHERE name = {Sqlite.Escape(name)};");
|
var res = Sqlite.Exec(db, $"SELECT * FROM personal_lists WHERE name = {Sqlite.Escape(name)};");
|
||||||
@@ -590,10 +681,33 @@ class TYTD.Downloader {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
public GetPersonalListTempUrl(name)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
var db = this.OpenDB();
|
||||||
|
var items = [];
|
||||||
|
|
||||||
|
var lists = Sqlite.Exec(db, $"SELECT * FROM personal_list_entries WHERE listName = {Sqlite.Escape(name)};");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
if(TypeIsList(lists))
|
||||||
|
{
|
||||||
|
var url = $"https://www.youtube.com/watch_videos?video_ids=";
|
||||||
|
var first = true;
|
||||||
|
each(var item : lists)
|
||||||
|
{
|
||||||
|
if(!first) url += $",{item.videoId}";
|
||||||
|
else url += item.videoId;
|
||||||
|
first=false;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
/^ ^/
|
/^ ^/
|
||||||
public GetPersonalListContents(name, offset, count)
|
public GetPersonalListContents(name, offset, count)
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@@ -621,6 +735,23 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public QueueRemoveItem(id)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = OpenDB();
|
||||||
|
Sqlite.Exec(db,$"DELETE FROM queue WHERE id = {Sqlite.Escape(id)};");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
}
|
||||||
|
public QueueClear()
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = OpenDB();
|
||||||
|
Sqlite.Exec(db,$"DELETE FROM queue;");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
}
|
||||||
|
|
||||||
public AddToPersonalList(name, id)
|
public AddToPersonalList(name, id)
|
||||||
{
|
{
|
||||||
@@ -716,6 +847,8 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
public VideoProgress = new TYTD.Event();
|
public VideoProgress = new TYTD.Event();
|
||||||
|
|
||||||
|
public BeforeQueued = new TYTD.Event();
|
||||||
|
|
||||||
public CurrentVideo = {
|
public CurrentVideo = {
|
||||||
Title = "N/A",
|
Title = "N/A",
|
||||||
Channel = "N/A",
|
Channel = "N/A",
|
||||||
@@ -741,12 +874,27 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
private DownloaderThreadHandle;
|
private DownloaderThreadHandle;
|
||||||
|
|
||||||
private Queue = new TYTD.Queue();
|
|
||||||
/^
|
/^
|
||||||
Get Video Queue count
|
Get Video Queue count
|
||||||
^/
|
^/
|
||||||
|
|
||||||
public getVideoQueueCount() this.Queue.Count;
|
public getVideoQueueCount() {
|
||||||
|
Mutex.Lock();
|
||||||
|
const db = OpenDB();
|
||||||
|
|
||||||
|
const res = Sqlite.Exec(db, "SELECT COUNT(*) FROM queue;");
|
||||||
|
|
||||||
|
Sqlite.Close(db);
|
||||||
|
|
||||||
|
Mutex.Unlock();
|
||||||
|
|
||||||
|
const queueCount = res[0].["COUNT(*)"];
|
||||||
|
if(TypeIsString(queueCount))
|
||||||
|
{
|
||||||
|
return ParseLong(queueCount);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private PlaylistThreadHandle;
|
private PlaylistThreadHandle;
|
||||||
|
|
||||||
@@ -770,7 +918,7 @@ class TYTD.Downloader {
|
|||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var _res = Sqlite.Exec(db,$"SELECT * FROM playlists p INNER JOIN playlist_entries e ON p.id = e.playlistId WHERE p.playlistId = {Sqlite.Escape(id)} LIMIT 1;");
|
var _res = Sqlite.Exec(db,$"SELECT * FROM playlists p INNER JOIN playlist_entries e ON p.id = e.playlistId WHERE p.playlistId = {Sqlite.Escape(id)} LIMIT 1;");
|
||||||
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
if(TypeOf(_res) == "List" && _res.Length > 0)
|
if(TypeOf(_res) == "List" && _res.Length > 0)
|
||||||
{
|
{
|
||||||
@@ -787,7 +935,7 @@ class TYTD.Downloader {
|
|||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var _res = Sqlite.Exec(db,$"SELECT * FROM videos WHERE channelId = {Sqlite.Escape(id)} LIMIT 1;");
|
var _res = Sqlite.Exec(db,$"SELECT * FROM videos WHERE channelId = {Sqlite.Escape(id)} LIMIT 1;");
|
||||||
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
if(TypeOf(_res) == "List" && _res.Length > 0)
|
if(TypeOf(_res) == "List" && _res.Length > 0)
|
||||||
{
|
{
|
||||||
@@ -930,6 +1078,7 @@ class TYTD.Downloader {
|
|||||||
while(this.Running)
|
while(this.Running)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
this.FlushExpired();
|
||||||
var res = this.PlaylistQueue.Pop();
|
var res = this.PlaylistQueue.Pop();
|
||||||
|
|
||||||
if(TypeOf(res) != "Null")
|
if(TypeOf(res) != "Null")
|
||||||
@@ -937,7 +1086,7 @@ class TYTD.Downloader {
|
|||||||
res();
|
res();
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentTime = DateTime.NowEpoch;
|
var currentTime = DateTime.NowEpoch ?? 0;
|
||||||
var bt = this.Config.BellTimer;
|
var bt = this.Config.BellTimer;
|
||||||
|
|
||||||
|
|
||||||
@@ -1008,7 +1157,12 @@ class TYTD.Downloader {
|
|||||||
var res = this.GetVideo(videoId);
|
var res = this.GetVideo(videoId);
|
||||||
if(res != null)
|
if(res != null)
|
||||||
this.Bell.Invoke(this,{
|
this.Bell.Invoke(this,{
|
||||||
Video = res
|
Video = {
|
||||||
|
VideoId = res.videoId,
|
||||||
|
ChannelId = res.channelId,
|
||||||
|
Title = res.title,
|
||||||
|
Channel = res.author
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(downloadRes != Resolution.NoDownload)
|
if(downloadRes != Resolution.NoDownload)
|
||||||
@@ -1052,31 +1206,35 @@ class TYTD.Downloader {
|
|||||||
while(this.Running)
|
while(this.Running)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
var res = this.Queue.Pop();
|
var res = PopQueue();
|
||||||
|
|
||||||
|
//Console.WriteLine(res);
|
||||||
|
|
||||||
|
|
||||||
if(TypeOf(res) != "Null")
|
if(TypeOf(res) != "Null")
|
||||||
{
|
{
|
||||||
|
if(TypeIsDefined(res.TYTD = this)){
|
||||||
res.TYTD = this;
|
res.Progress = (progress)=>{
|
||||||
res.Progress = (progress)=>{
|
this.CurrentVideoProgress = progress;
|
||||||
this.CurrentVideoProgress = progress;
|
this.VideoProgress.Invoke(this, {
|
||||||
this.VideoProgress.Invoke(this, {
|
Video = res.Video,
|
||||||
Video = res.Video,
|
progress
|
||||||
progress
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.CurrentVideo = res.Video;
|
||||||
|
|
||||||
|
this.VideoStarted.Invoke(this,{
|
||||||
|
Video = res.Video
|
||||||
});
|
});
|
||||||
};
|
res.Start();
|
||||||
|
|
||||||
this.CurrentVideo = res.Video;
|
this.VideoEnded.Invoke(this,{
|
||||||
|
Video = res.Video
|
||||||
|
});
|
||||||
|
|
||||||
this.VideoStarted.Invoke(this,{
|
}
|
||||||
Video = res.Video
|
|
||||||
});
|
|
||||||
|
|
||||||
res.Start();
|
|
||||||
|
|
||||||
this.VideoEnded.Invoke(this,{
|
|
||||||
Video = res.Video
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
try{
|
try{
|
||||||
@@ -1258,17 +1416,68 @@ class TYTD.Downloader {
|
|||||||
if(e == null)
|
if(e == null)
|
||||||
{
|
{
|
||||||
var req = this.ManifestRequest(id);
|
var req = this.ManifestRequest(id);
|
||||||
|
|
||||||
this.PutVideoInfo(req.playerResponse.videoDetails);
|
this.PutVideoInfo(req.playerResponse.videoDetails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private DownloadCaptions(req)
|
||||||
|
{
|
||||||
|
const tracks = req.playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks;
|
||||||
|
|
||||||
|
if(TypeIsList(tracks))
|
||||||
|
{
|
||||||
|
each(var item : tracks)
|
||||||
|
{
|
||||||
|
if(!TypeIsString(item.languageCode)) continue;
|
||||||
|
if(!TypeIsString(item.baseUrl)) continue;
|
||||||
|
try {
|
||||||
|
var path = /"Streams"/id.Substring(0,4) / id.Substring(4) / item.languageCode;
|
||||||
|
if(!this.Storage.FileExists(path+".xml"))
|
||||||
|
{
|
||||||
|
var resp = Net.Http.MakeRequest(url,{FollowRedirects=true});
|
||||||
|
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||||
|
{
|
||||||
|
const strm=this.Storage.OpenFile(path+".xml","wb");
|
||||||
|
resp.CopyToStream(strm);
|
||||||
|
strm.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!this.Storage.FileExists(path+".vtt"))
|
||||||
|
{
|
||||||
|
var resp = Net.Http.MakeRequest(url.Replace("fmt=srv3","fmt=vtt"),{FollowRedirects=true});
|
||||||
|
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||||
|
{
|
||||||
|
const strm=this.Storage.OpenFile(path+".vtt","wb");
|
||||||
|
resp.CopyToStream(strm);
|
||||||
|
strm.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!this.Storage.FileExists(path+".srt"))
|
||||||
|
{
|
||||||
|
var resp = Net.Http.MakeRequest(url.Replace("fmt=srv3","fmt=srt"),{FollowRedirects=true});
|
||||||
|
|
||||||
|
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||||
|
{
|
||||||
|
const strm=this.Storage.OpenFile(path+".srt","wb");
|
||||||
|
resp.CopyToStream(strm);
|
||||||
|
strm.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(ex) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/^
|
/^
|
||||||
Put video info from info into database
|
Put video info from info into database
|
||||||
^/
|
^/
|
||||||
public PutVideoInfo(info)
|
public PutVideoInfo(info)
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
|
|
||||||
var keywords = info.keywords;
|
var keywords = info.keywords;
|
||||||
@@ -1286,7 +1495,7 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
private InitDatabase()
|
private InitDatabase()
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS videos (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT UNIQUE, title TEXT, lengthSeconds INTEGER, keywords TEXT, channelId TEXT, shortDescription TEXT, viewCount INTEGER, author TEXT, addDate INTEGER, tytdTag TEXT);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS videos (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT UNIQUE, title TEXT, lengthSeconds INTEGER, keywords TEXT, channelId TEXT, shortDescription TEXT, viewCount INTEGER, author TEXT, addDate INTEGER, tytdTag TEXT);");
|
||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlists (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId TEXT UNIQUE,channelId TEXT,channelTitle TEXT, title TEXT);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlists (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId TEXT UNIQUE,channelId TEXT,channelTitle TEXT, title TEXT);");
|
||||||
@@ -1297,7 +1506,11 @@ class TYTD.Downloader {
|
|||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS plugin_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, extension TEXT, key TEXT, value TEXT, UNIQUE(extension,key) ON CONFLICT REPLACE);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS plugin_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, extension TEXT, key TEXT, value TEXT, UNIQUE(extension,key) ON CONFLICT REPLACE);");
|
||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
|
||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT, password_salt TEXT, flags INTEGER);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT, password_salt TEXT, flags INTEGER);");
|
||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key TEXT UNIQUE);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key TEXT UNIQUE, expires INTEGER);");
|
||||||
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS sso (id INTEGER PRIMARY KEY AUTOINCREMENT, service_name TEXT UNIQUE, service_pretty_name TEXT, sso_app_key TEXT UNIQUE, service_auth_post TEXT, service_auth_redirect TEXT);");
|
||||||
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT, resolution TEXT);");
|
||||||
|
Sqlite.Exec(db,"ALTER TABLE sessions ADD expires INTEGER;");
|
||||||
|
Sqlite.Exec(db,"DELETE FROM sessions WHERE expires IS NULL;");
|
||||||
var config=Sqlite.Exec(db,"SELECT * FROM plugin_settings WHERE extension = '' AND key = 'settings';");
|
var config=Sqlite.Exec(db,"SELECT * FROM plugin_settings WHERE extension = '' AND key = 'settings';");
|
||||||
if(TypeOf(config) == "List" && config.Length>0)
|
if(TypeOf(config) == "List" && config.Length>0)
|
||||||
{
|
{
|
||||||
@@ -1318,9 +1531,9 @@ class TYTD.Downloader {
|
|||||||
}
|
}
|
||||||
private _setPluginValue(extension,key,value)
|
private _setPluginValue(extension,key,value)
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
Sqlite.Exec(db, $"INSERT INTO plugin_settings (extension,key,value) VALUES ({Sqlite.Escape(extension)},{Sqlite.Escape(key)},{Sqlite.Escape(value)});");
|
Sqlite.Exec(db, $"INSERT OR REPLACE INTO plugin_settings (extension,key,value) VALUES ({Sqlite.Escape(extension)},{Sqlite.Escape(key)},{Sqlite.Escape(value)});");
|
||||||
Sqlite.Close(db);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
}
|
}
|
||||||
@@ -1522,6 +1735,7 @@ class TYTD.Downloader {
|
|||||||
^/
|
^/
|
||||||
public ManifestRequest(vid)
|
public ManifestRequest(vid)
|
||||||
{
|
{
|
||||||
|
for(var tries=0; tries<5;tries++) {
|
||||||
var id = TYTD.GetVideoId(vid);
|
var id = TYTD.GetVideoId(vid);
|
||||||
if(id == null) return null;
|
if(id == null) return null;
|
||||||
TryDownloadVideoThumbnail(id,"0");
|
TryDownloadVideoThumbnail(id,"0");
|
||||||
@@ -1539,7 +1753,7 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
Key = "User-Agent",
|
Key = "User-Agent",
|
||||||
|
|
||||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
Value = "com.google.android.youtube/21.03.36 (Linux; U; Android 16; GB) gzip"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
Body = Net.Http.TextHttpRequestBody(embed("request.json").ToString(),"application/json")
|
Body = Net.Http.TextHttpRequestBody(embed("request.json").ToString(),"application/json")
|
||||||
@@ -1557,17 +1771,55 @@ class TYTD.Downloader {
|
|||||||
RequestHeaders = [
|
RequestHeaders = [
|
||||||
{
|
{
|
||||||
Key = "User-Agent",
|
Key = "User-Agent",
|
||||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
Value = "com.google.android.youtube/21.03.36 (Linux; U; Android 16; GB) gzip"
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
Body = Net.Http.TextHttpRequestBody(embed("request2.json").ToString().Replace("VIDEO_ID_HERE", id).Replace("VISITOR_DATA",visitor),"application/json")
|
Body = Net.Http.TextHttpRequestBody(embed("request2.json").ToString().Replace("VIDEO_ID_HERE", id).Replace("VISITOR_DATA",visitor),"application/json")
|
||||||
};
|
};
|
||||||
this.RateLimit();
|
|
||||||
var response = Net.Http.MakeRequest(url,requestData);
|
var response = Net.Http.MakeRequest(url,requestData);
|
||||||
if(response.StatusCode < 200 || response.StatusCode > 299) return null;
|
|
||||||
return Json.Decode(response.ReadAsString());
|
if(response.StatusCode < 200 || response.StatusCode > 299) {
|
||||||
|
if(tries == 4)
|
||||||
|
{
|
||||||
|
const respText = response.ReadAsString();
|
||||||
|
throw new VideoDownloadError(id, $"StatusCode does not indicate success {response.StatusCode}\n{respText}");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const respText = response.ReadAsString();
|
||||||
|
const jsonResp = Json.Decode(respText);
|
||||||
|
if(!TypeIsDictionary(jsonResp.playerResponse)) {
|
||||||
|
if(tries == 4)
|
||||||
|
{
|
||||||
|
throw new VideoDownloadError(id, $"Player response is not defined, StatusCode: {response.StatusCode}");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(TypeIsDictionary(jsonResp.playerResponse.playabilityStatus))
|
||||||
|
{
|
||||||
|
if(jsonResp.playerResponse.playabilityStatus.status == "ERROR")
|
||||||
|
{
|
||||||
|
throw new VideoDownloadError(id, jsonResp.playerResponse.playabilityStatus.reason);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new VideoDownloadError(id, "playabilityStatus is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!TypeIsDictionary(jsonResp.playerResponse.videoDetails))
|
||||||
|
{
|
||||||
|
Console.WriteLine("YEY");
|
||||||
|
throw new VideoDownloadError(id, "videoDetails is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
//if(!TypeIsList(jsonResp.playerResponse.streamingData.adaptiveFormats))
|
||||||
|
//{
|
||||||
|
// throw new VideoDownloadError(id, "adaptiveFormats is missing");
|
||||||
|
//}
|
||||||
|
this.DownloadCaptions(jsonResp);
|
||||||
|
return jsonResp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1659,7 +1911,7 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
var first = true;
|
var first = true;
|
||||||
|
|
||||||
/*this.Muxex.Lock();
|
/*this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var d = $"INSERT INTO videos (videoId,title,lengthSeconds,keywords,channelId,shortDescription,viewCount,author,addDate,tytdTag) VALUES ({Sqlite.Escape(info.videoId)},{Sqlite.Escape(info.title)},{info.lengthSeconds},{Sqlite.Escape(info.keywords.ToString())},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});";
|
var d = $"INSERT INTO videos (videoId,title,lengthSeconds,keywords,channelId,shortDescription,viewCount,author,addDate,tytdTag) VALUES ({Sqlite.Escape(info.videoId)},{Sqlite.Escape(info.title)},{info.lengthSeconds},{Sqlite.Escape(info.keywords.ToString())},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});";
|
||||||
Sqlite.Exec(db, d);
|
Sqlite.Exec(db, d);
|
||||||
@@ -1686,7 +1938,7 @@ class TYTD.Downloader {
|
|||||||
};
|
};
|
||||||
*/
|
*/
|
||||||
if(first) {
|
if(first) {
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
if(isPlaylist)
|
if(isPlaylist)
|
||||||
{
|
{
|
||||||
@@ -1722,7 +1974,7 @@ class TYTD.Downloader {
|
|||||||
ids.Add(vid);
|
ids.Add(vid);
|
||||||
if(TypeOf(dbRow) == "Long")
|
if(TypeOf(dbRow) == "Long")
|
||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
Sqlite.Exec(db,$"INSERT INTO playlist_entries (playlistId,videoId) VALUES ({dbRow},{Sqlite.Escape(vid)});");
|
Sqlite.Exec(db,$"INSERT INTO playlist_entries (playlistId,videoId) VALUES ({dbRow},{Sqlite.Escape(vid)});");
|
||||||
Sqlite.Close(db);
|
Sqlite.Close(db);
|
||||||
@@ -1743,21 +1995,21 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
private requests = 0;
|
private requests = 0;
|
||||||
|
|
||||||
private rlm=new Muxex();
|
private rlm=new Mutex();
|
||||||
|
|
||||||
private RateLimit()
|
private RateLimit()
|
||||||
{
|
{
|
||||||
this.rlm.Lock();
|
this.rlm.Lock();
|
||||||
var curRequest = DateTime.NowEpoch;
|
var curRequest = DateTime.NowEpoch;
|
||||||
if((curRequest - this.lastRequest) > 60)
|
if((curRequest - this.lastRequest) > 10)
|
||||||
{
|
{
|
||||||
this.requests = 0;
|
this.requests = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.requests++;
|
this.requests++;
|
||||||
if(this.requests > 5)
|
if(this.requests >= 1)
|
||||||
{
|
{
|
||||||
DateTime.Sleep(25000);
|
DateTime.Sleep(1500);
|
||||||
this.requests=0;
|
this.requests=0;
|
||||||
curRequest = DateTime.NowEpoch;
|
curRequest = DateTime.NowEpoch;
|
||||||
}
|
}
|
||||||
@@ -1798,7 +2050,6 @@ class TYTD.Downloader {
|
|||||||
const token = GetSessionToken(ctx);
|
const token = GetSessionToken(ctx);
|
||||||
if(TypeIsString(token))
|
if(TypeIsString(token))
|
||||||
{
|
{
|
||||||
|
|
||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
const db = this.OpenDB();
|
const db = this.OpenDB();
|
||||||
Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(token)};");
|
Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(token)};");
|
||||||
@@ -1831,7 +2082,17 @@ class TYTD.Downloader {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
public FlushExpired()
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
const currentTime = DateTime.NowEpoch ?? 0;
|
||||||
|
const sessions = Sqlite.Exec(db, $"DELETE FROM sessions WHERE expires != 0 AND expires < {currentTime};");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
public IsLoggedIn(ctx)
|
public IsLoggedIn(ctx)
|
||||||
{
|
{
|
||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
@@ -1852,13 +2113,32 @@ class TYTD.Downloader {
|
|||||||
if(TypeIsString(sessionToken))
|
if(TypeIsString(sessionToken))
|
||||||
{
|
{
|
||||||
const res = Sqlite.Exec(db, $"SELECT * FROM sessions s INNER JOIN users u ON s.accountId = u.id WHERE key = {Sqlite.Escape(sessionToken)};");
|
const res = Sqlite.Exec(db, $"SELECT * FROM sessions s INNER JOIN users u ON s.accountId = u.id WHERE key = {Sqlite.Escape(sessionToken)};");
|
||||||
|
|
||||||
|
|
||||||
if(TypeIsList(res))
|
if(TypeIsList(res))
|
||||||
each(var item : res)
|
each(var item : res)
|
||||||
{
|
{
|
||||||
|
const whenItExpires = ParseLong(item.expires);
|
||||||
|
const currentTime = DateTime.NowEpoch ?? 0;
|
||||||
|
if(whenItExpires != 0 && currentTime < whenItExpires && (whenItExpires - currentTime) < (UserFlags.Expires-3600))
|
||||||
|
{
|
||||||
|
const expiry = currentTime + UserFlags.Expires;
|
||||||
|
Sqlite.Exec(db, $"UPDATE sessions SET expires = {expiry} WHERE key = {Sqlite.Escape(sessionToken)};");
|
||||||
|
|
||||||
|
ctx.WithHeader("Set-Cookie",$"Session={sessionToken}; SameSite=Lax; Expires={new DateTime(expiry).ToHttpDate()}; HttpOnly");
|
||||||
|
}
|
||||||
|
else if(whenItExpires != 0 && currentTime >= whenItExpires)
|
||||||
|
{
|
||||||
|
|
||||||
|
Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(sessionToken)};");
|
||||||
|
item.flags = 0;
|
||||||
|
}
|
||||||
Sqlite.Close(db);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
|
|
||||||
return ParseLong(item.flags) | 1;
|
return ParseLong(item.flags) | 1;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1867,9 +2147,27 @@ class TYTD.Downloader {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GetSSO(appname)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
const res = Sqlite.Exec(db, $"SELECT * FROM sso WHERE service_name = {Sqlite.Escape(appname)}");
|
||||||
|
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
if(TypeIsList(res))
|
||||||
|
{
|
||||||
|
each(var item : res)
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public WhoAmI(ctx)
|
public WhoAmI(ctx)
|
||||||
{
|
{
|
||||||
his.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
const db = this.OpenDB();
|
const db = this.OpenDB();
|
||||||
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
|
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
|
||||||
var noAccounts=true;
|
var noAccounts=true;
|
||||||
@@ -1891,6 +2189,20 @@ class TYTD.Downloader {
|
|||||||
if(TypeIsList(res))
|
if(TypeIsList(res))
|
||||||
each(var item : res)
|
each(var item : res)
|
||||||
{
|
{
|
||||||
|
const whenItExpires = ParseLong(item.expires);
|
||||||
|
const currentTime = DateTime.NowEpoch ?? 0;
|
||||||
|
if(whenItExpires != 0 && currentTime < whenItExpires && (whenItExpires - currentTime) < (UserFlags.Expires-3600))
|
||||||
|
{
|
||||||
|
const expiry = currentTime + UserFlags.Expires;
|
||||||
|
Sqlite.Exec(db, $"UPDATE sessions SET expires = {expiry} WHERE key = {Sqlite.Escape(sessionToken)};");
|
||||||
|
|
||||||
|
ctx.WithHeader("Set-Cookie",$"Session={sessionToken}; SameSite=Lax; Expires={new DateTime(expiry).ToHttpDate()}");
|
||||||
|
}
|
||||||
|
else if(whenItExpires != 0 && currentTime >= whenItExpires)
|
||||||
|
{
|
||||||
|
Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(sessionToken)};");
|
||||||
|
item.flags = "0";
|
||||||
|
}
|
||||||
Sqlite.Close(db);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
item.flags = ParseLong(item.flags);
|
item.flags = ParseLong(item.flags);
|
||||||
@@ -1905,9 +2217,9 @@ class TYTD.Downloader {
|
|||||||
public Passwd(ctx, oldPassword, newPassword, logout)
|
public Passwd(ctx, oldPassword, newPassword, logout)
|
||||||
{
|
{
|
||||||
const whoami = this.WhoAmI(ctx);
|
const whoami = this.WhoAmI(ctx);
|
||||||
if(TypeIsDictionary(user) && TypeIsString(item.password_salt))
|
if(whoami.flags != 0 && TypeIsDictionary(whoami) && TypeIsString(whoami.password_salt))
|
||||||
{
|
{
|
||||||
var salt = Crypto.Base64Decode(item.password_salt);
|
var salt = Crypto.Base64Decode(whoami.password_salt);
|
||||||
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
var hashStr = Crypto.Base64Encode(hash);
|
var hashStr = Crypto.Base64Encode(hash);
|
||||||
|
|
||||||
@@ -1937,17 +2249,45 @@ class TYTD.Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
return { success=false, reason = "Unable to login for some reason, maybe your token expired"};
|
||||||
}
|
}
|
||||||
public Login(username, password)
|
public Auth(username, password)
|
||||||
{
|
{
|
||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
const db = this.OpenDB();
|
const db = this.OpenDB();
|
||||||
const user = Sqlite.Exec(db, $"SELECT * FROM users WHERE username = {Sqlite.Escape(username)};");
|
const user = Sqlite.Exec(db, $"SELECT * FROM users WHERE username = {Sqlite.Escape(username)};");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
if(TypeIsList(user))
|
||||||
|
{
|
||||||
|
each(var item : user)
|
||||||
|
{
|
||||||
|
|
||||||
|
var salt = Crypto.Base64Decode(item.password_salt);
|
||||||
|
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
|
var hashStr = Crypto.Base64Encode(hash);
|
||||||
|
|
||||||
|
if(item.password_hash == hashStr)
|
||||||
|
{
|
||||||
|
return {flags = ParseLong(item.flags)};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public Login(username, password, doesExpire)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
const user = Sqlite.Exec(db, $"SELECT * FROM users WHERE username = {Sqlite.Escape(username)};");
|
||||||
|
|
||||||
if(TypeIsList(user))
|
if(TypeIsList(user))
|
||||||
{
|
{
|
||||||
each(var item : user)
|
each(var item : user)
|
||||||
{
|
{
|
||||||
this.Mutex.Unlock();
|
|
||||||
|
|
||||||
var salt = Crypto.Base64Decode(item.password_salt);
|
var salt = Crypto.Base64Decode(item.password_salt);
|
||||||
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
@@ -1956,18 +2296,22 @@ class TYTD.Downloader {
|
|||||||
if(item.password_hash == hashStr)
|
if(item.password_hash == hashStr)
|
||||||
{
|
{
|
||||||
var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025")));
|
var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025")));
|
||||||
this.Mutex.Lock();
|
|
||||||
const dbCon = this.OpenDB();
|
const expires = doesExpire ? ((DateTime.NowEpoch??0) + UserFlags.Expires) : 0;
|
||||||
Sqlite.Exec(dbCon, $"INSERT INTO sessions (accountId,key) VALUES ({item.id},{Sqlite.Escape(rand)});");
|
Sqlite.Exec(db, $"INSERT INTO sessions (accountId,key,expires) VALUES ({item.id},{Sqlite.Escape(rand)},{expires});");
|
||||||
Sqlite.Close(dbCon);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
return rand;
|
return rand;
|
||||||
}
|
}
|
||||||
|
Sqlite.Close(db);
|
||||||
|
|
||||||
|
this.Mutex.Unlock();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Sqlite.Close(db);
|
||||||
|
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1992,6 +2336,25 @@ class TYTD.Downloader {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RegisterSSO(req)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
/*
|
||||||
|
service_name TEXT UNIQUE, service_pretty_name TEXT, sso_app_key TEXT UNIQUE, service_auth_post TEXT, service_auth_redirect TEXT
|
||||||
|
*/
|
||||||
|
const resp = Sqlite.Exec(db, $"INSERT INTO sso (service_name, service_pretty_name, sso_app_key, service_auth_post, service_auth_redirect) VALUES ({Sqlite.Escape(req.service_name)},{Sqlite.Escape(req.service_pretty_name)},{Sqlite.Escape(req.sso_app_key)},{Sqlite.Escape(req.service_auth_post)}, {Sqlite.Escape(req.service_auth_redirect)});");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
if(TypeIsList(resp))
|
||||||
|
{
|
||||||
|
return { success=true};
|
||||||
|
}
|
||||||
|
else if(TypeIsString(resp))
|
||||||
|
{
|
||||||
|
return { success = false, reason = resp , type="db"};
|
||||||
|
}
|
||||||
|
return {success = false, reason = "Unknown", type ="db"};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
4
changeme.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 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.0"
|
||||||
|
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
|
||||||