Compare commits

...

12 Commits

Author SHA1 Message Date
d2f855881c Make it a little faster, queue is in sqlite now, fix db bug and more
All checks were successful
Build and Deploy on Tag / 🔨 Build (push) Successful in 10s
2026-06-25 15:43:00 -05:00
454a259e37 Make it a little faster, queue is in sqlite now, fix db bug and more
Some checks failed
Build and Deploy on Tag / 🔨 Build (push) Failing after 8s
2026-06-25 15:41:26 -05:00
c82957cc61 Make it a little faster, queue is in sqlite now, fix db bug and more
Some checks failed
Build and Deploy on Tag / 🔨 Build for everything else (push) Failing after 11s
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Failing after 20s
2026-06-25 15:35:39 -05:00
ede1f92981 Fix tytd config 2026-03-05 11:10:21 -06:00
90f33d455f Fix youtube breakage hopefully 2026-03-03 08:53:55 -06:00
ecb1496a54 Get diagnostics 2026-03-03 08:41:59 -06:00
39709662af Fix the captions downloading 2026-03-02 02:46:51 -06:00
29fdf6a2f3 Fix the captions downloading 2026-03-02 02:39:33 -06:00
44c87e1e50 Make the downloader more resiliant 2026-03-02 02:23:46 -06:00
8851f710fb Make requests slower 2026-03-01 10:22:07 -06:00
28b7138547 Add pwa support 2026-02-28 06:32:39 -06:00
02b10131f9 Fix bugs and make the readme better 2026-01-29 16:40:33 -06:00
70 changed files with 1664 additions and 942 deletions

59
.gitea/workflows/work.yml Normal file
View 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
View File

@@ -1,4 +1,8 @@
bin
obj
publish
TYTD
TYTD
artifacts
*.pkg.tar.zst
*.deb
*.rpm

View File

@@ -1,9 +1,14 @@
FROM onedev.site.tesses.net/crosslang/crosslang:latest
RUN mkdir /app
COPY Tesses.YouTubeDownloader.Server/bin/ /app
FROM git.tesses.org/tesses50/crosslang-withshell:latest AS build
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
ENV TYTDDIR=/data
EXPOSE 3255
ENTRYPOINT ["crossvm","/app/Tesses.YouTubeDownloader.Server-1.0.0.0-prod.crvm","--port=3255"]
ENTRYPOINT ["crossvm","/app/tytd2025.crvm","--port=3255"]

View File

@@ -1,39 +1,71 @@
# 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
![The Image](https://crosslang.tesseslanguage.com/images/tytdscreenshot.jpg)
[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
Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
from source:
```bash
cd Tesses.YouTubeDownloader.Server
git clone https://onedev.site.tesses.net/tytd2025
cd tytd2025/Tesses.YouTubeDownloader.Server
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:
```bash
crosslang install-webapp Tesses.YouTubeDownloader.Server
```
# Save to Videos
## To Run
Save to videos:
```bash
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
crosslang webapp tytd2025 --port=3255
```
# Save anywhere (windows, replace C:\path\to\tytd with the folder you want)
SET TYTDDIR=/path/to/tytd
Save to a folder (windows):
```batch
REM Replace C:\path\to\tytd with the folder you want
SET TYTDDIR=C:\path\to\tytd
crosslang webapp tytd2025 --port=3255
```

View File

@@ -28,6 +28,6 @@
"template_icon": "icon.png"
},
"name": "Tesses.YouTubeDownloader.PluginTemplate",
"version": "1.0.0.0-prod",
"version": "1.0.0.0-dev",
"icon": "icon.png"
}

View File

@@ -1,5 +1,5 @@
{
"icon": "icon.png",
"icon": "tytd-128.png",
"info": {
"description": "Download YouTube Videos (using CrossLang, via web interface, great for homelabs)",
"maintainer": "Mike Nolan",
@@ -14,5 +14,6 @@
"project_dependencies": [
"..\/Tesses.YouTubeDownloader"
],
"version": "1.0.0.0-dev"
"version": "1.0.0.0-dev",
"compTime": "secure"
}

View File

@@ -0,0 +1,4 @@
<div>
<h1>Cannot connect to the TYTD2025 server.</h1>
Refresh page to add to offline queue
</div>

View 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>

View 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");
}
}
}

View 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');
}
})
);
});

View 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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -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 "";
}

View File

@@ -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>;
}

View File

@@ -1,4 +0,0 @@
func Components.MusicArtist(item)
{
return <a href={$"./music-artist?id={Net.Http.UrlEncode(item.id)}"}>{item.name}</a>;
}

View File

@@ -20,11 +20,19 @@ func Components.PersonalListDescription(tytd,name,editing)
</form>
</true>
<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="max">
<plink(description)>
</div>
<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>
</div>
</div>

View File

@@ -1,13 +1,12 @@
var progress=0;
func Components.Progress(tytd)
{
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>
<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>
</div>;
progress++;
return html;
}

View File

@@ -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;
}

View File

@@ -1,44 +1,68 @@
func Components.Shell(title, html, page, $mypage)
{
if(TypeOf(mypage) != "Path")
mypage = /;
var index = (/).MakeRelative(mypage).ToString();
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 = [
{
text="Home",
icon="home",
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",
icon = "download",
href=(/"downloads").MakeRelative(mypage).ToString(),
classStr=""
classStr="",
badge=""
},
{
text = "Plugins",
icon = "extension",
href= (/"plugins").MakeRelative(mypage).ToString(),
classStr=""
classStr="",
badge=""
},
{
text = "Settings",
icon = "settings",
href= (/"settings").MakeRelative(mypage).ToString(),
classStr=""
classStr="",
badge=""
},
{
text = "Api",
icon = "api",
href = (/"api").MakeRelative(mypage).ToString(),
classStr=""
classStr="",
badge=""
}
];
pages[page].classStr = "active";
pages[page].classStr = " primary-container";
return <!DOCTYPE html>
<html lang="en">
<head>
@@ -47,19 +71,66 @@ func Components.Shell(title, html, page, $mypage)
<title>TYTD - {title}</title>
<link rel="stylesheet" href={(/"beer.min.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={(/"offline.js").MakeRelative(mypage).ToString()} defer></script>
<script type="module" src={(/"beer.min.js").MakeRelative(mypage).ToString()} defer></script>
</head>
<body hx-indicator="#loading-indicator">
<nav class="bottom">
<dialog class="left" id="leftpane">
<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)>
<a hx-get={page.href} hx-target="body" hx-push-url="true" class={page.classStr}>
<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>
</a>
</each>
</nav>
</nav>*/
<main class="responsive">
<raw(html)>
@@ -69,7 +140,7 @@ func Components.Shell(title, html, page, $mypage)
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
<img class="responsive" src="./tytd.svg">
</div>
<raw(service_worker_script)>
</body>
</html>;
}

View File

@@ -1,3 +1,5 @@
const BUILD_TIME = comptime DateTime.NowEpoch;
var TYTDResources = [
{path="/beer.min.css",value=embed("beer.min.css")},
{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="/favicon.ico",value=embed("favicon.ico")},
{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="/theme.css",value=embed("theme.css")},
{path="/video.min.js",value=embed("video.min.js")},
{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;
class TYTDApp {
@@ -29,21 +50,71 @@ class TYTDApp {
public TYTDApp()
{
Console.WriteLine($"Built at {new DateTime(BUILD_TIME).ToString()}");
var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir());
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir()));
this.TYTD.Start();
}
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.Method == "POST")
{
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)
{
ctx.SendJson({
@@ -70,14 +141,17 @@ class TYTDApp {
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
if(ctx.Method == "POST")
{
const result = this.TYTD.Login(username, password);
const result = this.TYTD.Login(username, password,true);
if(result)
{
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;
}
else incorrect=true;
@@ -97,6 +171,29 @@ class TYTDApp {
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.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}");
return true;
@@ -151,7 +248,7 @@ class TYTDApp {
case 3:
{
const createUser = ctx.QueryParams.GetFirstBoolean("user");
const createUser = (ctx.QueryParams.TryGetFirst("user") ?? "no")=="yes";
var seconds = this.OOBE_STATE.pollSeconds;
seconds += this.OOBE_STATE.pollMinutes * 60;
@@ -208,7 +305,6 @@ class TYTDApp {
return true;
}
}
if(ctx.Path == "/welcome")
{
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings";
@@ -245,11 +341,49 @@ class TYTDApp {
return true;
}
}
if(ctx.Path == "/")
{
ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD));
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")
{
var incorrect=false;
@@ -265,6 +399,7 @@ class TYTDApp {
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
}
else if(ctx.Path == "/newuser")
{
var error = null;
@@ -340,6 +475,36 @@ class TYTDApp {
}
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")
{
var v = ctx.QueryParams.TryGetFirst("v");
@@ -371,6 +536,20 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(jo);
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")
{
var id = ctx.QueryParams.TryGetFirst("v");
@@ -400,6 +579,21 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count));
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")
{
var q = ctx.QueryParams.TryGetFirst("q");
@@ -435,10 +629,34 @@ class TYTDApp {
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;
}
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")
{
/*
@@ -579,6 +797,24 @@ class TYTDApp {
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")
{
ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx));
@@ -599,7 +835,7 @@ class TYTDApp {
{
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;
}
const user = ctx.QueryParams.TryGetFirst("user");
@@ -677,7 +913,7 @@ class TYTDApp {
{
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;
}
if(ctx.Method == "POST")
@@ -714,6 +950,36 @@ class TYTDApp {
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
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")
{
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));
return true;
}
else if(ctx.Path == "/queue-size")
{
ctx.WithMimeType("text/html").SendText(Components.QueueSZ(this.TYTD));
return true;
}
else if(ctx.Path == "/plugins")
{
ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx));

View File

@@ -72,5 +72,5 @@ func Pages.Admin(tytd,ctx)
</form>;
return Components.Shell("Admin Settings",html ,3);
return Components.Shell("Admin Settings",html ,4);
}

View File

@@ -8,5 +8,5 @@ func Pages.Api()
</a>
</ul>
</null>;
return Components.Shell("Api",html,4);
return Components.Shell("Api",html,5);
}

View File

@@ -21,7 +21,7 @@ func Pages.ApiV1()
</fieldset>
</each>
</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());

View File

@@ -45,5 +45,5 @@ const html = <null>
</form>
</null>;
return Components.Shell("Change your password",html,3);
return Components.Shell("Change your password",html,4);
}

View File

@@ -35,5 +35,5 @@ func Pages.ChannelInfo(tytd,ctx)
title = res.authorName;
}
return Components.Shell($"Channel {title}",html,1);
return Components.Shell($"Channel {title}",html,2);
}

View File

@@ -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);
}

View File

@@ -289,5 +289,5 @@ func Pages.Downloads(tytd,ctx)
return Components.Shell($"Downloads {q}",html,1);
return Components.Shell($"Downloads {q}",html,2);
}

View File

@@ -38,7 +38,7 @@ func Pages.EditUserList(tytd, ctx)
</fieldset>
</null>;
return Components.Shell("Edit users",html, 3);
return Components.Shell("Edit users",html, 4);
}
func Pages.EditUser(user)
@@ -141,5 +141,5 @@ func Pages.EditUser(user)
</if>;
return Components.Shell("Edit user",html, 3);
return Components.Shell("Edit user",html, 4);
}

View File

@@ -35,5 +35,5 @@ func Pages.List(tytd,ctx)
</null>;
return Components.Shell($"Personal List {name}",html,1);
return Components.Shell($"Personal List {name}",html,2);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -81,5 +81,5 @@ func Pages.NewUser(error)
</form>
</null>;
return Components.Shell("New User",html ,3);
return Components.Shell("New User",html ,4);
}

View File

@@ -12,7 +12,9 @@ func Pages.OobePage3()
</div>
<hr class="max">
<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>
<hr class="max">

View File

@@ -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);
}

View File

@@ -35,5 +35,5 @@ func Pages.PlaylistInfo(tytd,ctx)
title = res.authorName;
}
return Components.Shell($"Playlist {title}",html,1);
return Components.Shell($"Playlist {title}",html,2);
}

View File

@@ -75,5 +75,5 @@ func Pages.DownloadPlugins(tytd,ctx)
</null>;
return Components.Shell("Download plugins",html ,2);
return Components.Shell("Download plugins",html ,3);
}

View File

@@ -20,5 +20,5 @@ func Pages.Plugins(tytd,ctx)
</each>
</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);
}

View 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);
}

View File

@@ -15,5 +15,5 @@ func Pages.Settings(tytd,ctx)
</fieldset>;
return Components.Shell("Settings",html ,3);
return Components.Shell("Settings",html ,4);
}

View File

@@ -27,5 +27,5 @@ func Pages.VideoInfo(tytd,ctx)
</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);
}

View File

@@ -3,7 +3,7 @@ func Pages.Welcome(redirect)
const html = <null>
<h1>You are all set</h1>
<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>
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)

View File

@@ -11,5 +11,5 @@ func Pages.WhoAmI(row)
<li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li>
</ul>
</null>;
return Components.Shell("Settings",html ,3);
return Components.Shell("Settings",html ,4);
}

View File

@@ -2,12 +2,12 @@
"context": {
"client": {
"clientName": "ANDROID",
"clientVersion": "19.28.35",
"clientVersion": "21.03.36",
"clientScreen": "WATCH",
"platform": "MOBILE",
"osName": "Android",
"osVersion": "15",
"androidSdkVersion": 35,
"osVersion": "16",
"androidSdkVersion": 36,
"hl": "en-GB",
"gl": "US",
"utcOffsetMinutes": 0

View File

@@ -2,13 +2,13 @@
"context": {
"client": {
"clientName": "ANDROID",
"clientVersion": "19.28.35",
"clientVersion": "21.03.36",
"clientScreen": "WATCH",
"platform": "MOBILE",
"visitorData": "VISITOR_DATA",
"osName": "Android",
"osVersion": "15",
"androidSdkVersion": 35,
"osVersion": "16",
"androidSdkVersion": 36,
"hl": "en-GB",
"gl": "US",
"utcOffsetMinutes": 0

View File

@@ -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();
}
}

View File

@@ -4,33 +4,21 @@ class Resolution
static getNoDownload() "NoDownload";
/^ Get the video/audio mp4 (muxed by YouTube) ^/
static getLowVideo() "LowVideo";
/^ Get the highest video stream ^/
static getVideoOnly() "VideoOnly";
/^ Get the highest audio stream ^/
static getAudioOnly() "AudioOnly";
/^ Get the highest audio stream (and convert to mp3) ^/
/^ Get the LowVideo stream (and convert to mp3) ^/
static getMP3() "MP3";
/^ Get the highest audio stream (and convert to flac) ^/
/^ Get the LowVideo stream (and convert to flac) ^/
static getFLAC() "FLAC";
/^ Get the highest video and then audio stream (and convert to mp4) ^/
static getMP4() "MP4";
/^ 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";
/^ Get the LowVideo stream (and convert to AMV for mp4 players like: https://www.ebay.com/sch/i.html?_nkw=AMV+mp3+player) ^/
static getAMV() "AMV";
static getResolutions()
{
return [
{name="Don't Download", value=Resolution.NoDownload},
{name="Low (muxed by YouTube)", value=Resolution.LowVideo,default=true},
{name="Highest video (no audio)", value=Resolution.VideoOnly},
{name="Highest audio (no video)", value=Resolution.AudioOnly},
{name="Convert to MP3", value=Resolution.MP3},
{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}
{name="Don't Download", value=Resolution.NoDownload,filename=null},
{name="Low (muxed by YouTube)", value=Resolution.LowVideo,default=true,filename="ytmux.mp4"},
{name="Convert to MP3", value=Resolution.MP3,filename="conv.mp3"},
{name="Convert to FLAC", value=Resolution.FLAC,filename="conv.flac"},
{name="Convert To AMV (for generic mp4 players)", value=Resolution.AMV,filename="conv.amv"}
];
}
}

View File

@@ -35,6 +35,8 @@ class UserFlags {
return false;
}
static getITTR() 35000;
static getExpires() 86400 * 7;
}
class TYTD.Downloader {
@@ -65,6 +67,50 @@ class TYTD.Downloader {
this.Storage = vfs;
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
id: video id or url
@@ -72,42 +118,37 @@ class TYTD.Downloader {
^/
public DownloadVideo(id,$res)
{
this.LOG($"Adding video: {TYTD.GetVideoId(id)}, original val: {id}");
switch(res)
const theVideoId = TYTD.GetVideoId(id);
if(TypeIsString(theVideoId))
{
case Resolution.NoDownload:
{
var id = TYTD.GetVideoId(id);
if(id != null)
PutVideoInfoIfNotExists(id);
if(!TypeIsDefined(res)) res = Resolution.LowVideo;
const ent = {
Id = theVideoId,
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
@@ -121,6 +162,19 @@ class TYTD.Downloader {
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.PlaylistQueue.Push(()=>{
each(var item : this.QueryPlaylistItems(pid,true))
@@ -145,6 +199,18 @@ class TYTD.Downloader {
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.PlaylistQueue.Push(()=>{
each(var item : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
@@ -182,6 +248,7 @@ class TYTD.Downloader {
var pid = TYTD.GetPlaylistId(url);
var cid = TYTD.GetChannelId(url);
var tmp = TYTD.GetYouTubeTempPlaylist(url);
if(vid != null)
{
@@ -195,7 +262,13 @@ class TYTD.Downloader {
{
this.DownloadChannel(cid,res);
}
else if(tmp != null)
{
each(var item : tmp)
{
this.DownloadItem(item,res);
}
}
}
/^
Redirect url to info page
@@ -204,6 +277,8 @@ class TYTD.Downloader {
{
var vid = TYTD.GetVideoId(url);
var pid = TYTD.GetPlaylistId(url);
var cid = TYTD.GetChannelId(url);
var tmp = TYTD.GetYouTubeTempPlaylistRedirect(url);
if(vid != null)
{
@@ -217,6 +292,10 @@ class TYTD.Downloader {
{
return $"./channel?id={Net.Http.UrlEncode(cid)}";
}
else if(tmp != null)
{
return tmp;
}
return "./";
}
@@ -231,6 +310,18 @@ class TYTD.Downloader {
if(views < 1000000000000) return $"{views/1000000000}B 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
@@ -239,7 +330,7 @@ class TYTD.Downloader {
^/
public GetVideos(query, offset, count)
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var q = Sqlite.Escape($"%{query}%");
@@ -274,7 +365,7 @@ class TYTD.Downloader {
^/
public GetPlaylists(query, offset, count)
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var q = Sqlite.Escape($"%{query}%");
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",
items = []
};
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var channelTitle = "";
var title = "";
@@ -364,7 +455,7 @@ class TYTD.Downloader {
authorName = "N/A",
items = []
};
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var authorName = "";
@@ -408,7 +499,7 @@ class TYTD.Downloader {
^/
public GetChannels(query, offset, count)
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var q = Sqlite.Escape($"%{query}%");
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);
if(id == null) return null;
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
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);
if(id == null) return null;
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
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);
if(id == null) return null;
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
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^/
public GetPersonalLists()
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var items = [];
@@ -565,7 +656,7 @@ class TYTD.Downloader {
/^ Set the description of a personal list ^/
public SetPersonalListDescription(name,description)
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
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 ^/
public GetPersonalListDescription(name)
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db, $"SELECT * FROM personal_lists WHERE name = {Sqlite.Escape(name)};");
@@ -590,10 +681,33 @@ class TYTD.Downloader {
}
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)
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var items = [];
@@ -621,6 +735,23 @@ class TYTD.Downloader {
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)
{
@@ -716,6 +847,8 @@ class TYTD.Downloader {
public VideoProgress = new TYTD.Event();
public BeforeQueued = new TYTD.Event();
public CurrentVideo = {
Title = "N/A",
Channel = "N/A",
@@ -741,12 +874,27 @@ class TYTD.Downloader {
private DownloaderThreadHandle;
private Queue = new TYTD.Queue();
/^
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;
@@ -770,7 +918,7 @@ class TYTD.Downloader {
this.Mutex.Lock();
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;");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(_res) == "List" && _res.Length > 0)
{
@@ -787,7 +935,7 @@ class TYTD.Downloader {
this.Mutex.Lock();
var db = this.OpenDB();
var _res = Sqlite.Exec(db,$"SELECT * FROM videos WHERE channelId = {Sqlite.Escape(id)} LIMIT 1;");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(_res) == "List" && _res.Length > 0)
{
@@ -930,6 +1078,7 @@ class TYTD.Downloader {
while(this.Running)
{
try {
this.FlushExpired();
var res = this.PlaylistQueue.Pop();
if(TypeOf(res) != "Null")
@@ -937,7 +1086,7 @@ class TYTD.Downloader {
res();
}
var currentTime = DateTime.NowEpoch;
var currentTime = DateTime.NowEpoch ?? 0;
var bt = this.Config.BellTimer;
@@ -1008,7 +1157,12 @@ class TYTD.Downloader {
var res = this.GetVideo(videoId);
if(res != null)
this.Bell.Invoke(this,{
Video = res
Video = {
VideoId = res.videoId,
ChannelId = res.channelId,
Title = res.title,
Channel = res.author
}
});
}
if(downloadRes != Resolution.NoDownload)
@@ -1052,31 +1206,35 @@ class TYTD.Downloader {
while(this.Running)
{
try {
var res = this.Queue.Pop();
var res = PopQueue();
//Console.WriteLine(res);
if(TypeOf(res) != "Null")
{
res.TYTD = this;
res.Progress = (progress)=>{
this.CurrentVideoProgress = progress;
this.VideoProgress.Invoke(this, {
Video = res.Video,
progress
if(TypeIsDefined(res.TYTD = this)){
res.Progress = (progress)=>{
this.CurrentVideoProgress = progress;
this.VideoProgress.Invoke(this, {
Video = res.Video,
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) {
try{
@@ -1258,17 +1416,68 @@ class TYTD.Downloader {
if(e == null)
{
var req = this.ManifestRequest(id);
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
^/
public PutVideoInfo(info)
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
var keywords = info.keywords;
@@ -1286,7 +1495,7 @@ class TYTD.Downloader {
private InitDatabase()
{
this.Muxex.Lock();
this.Mutex.Lock();
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 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 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 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';");
if(TypeOf(config) == "List" && config.Length>0)
{
@@ -1318,9 +1531,9 @@ class TYTD.Downloader {
}
private _setPluginValue(extension,key,value)
{
this.Muxex.Lock();
this.Mutex.Lock();
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);
this.Mutex.Unlock();
}
@@ -1522,6 +1735,7 @@ class TYTD.Downloader {
^/
public ManifestRequest(vid)
{
for(var tries=0; tries<5;tries++) {
var id = TYTD.GetVideoId(vid);
if(id == null) return null;
TryDownloadVideoThumbnail(id,"0");
@@ -1539,7 +1753,7 @@ class TYTD.Downloader {
{
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")
@@ -1557,17 +1771,55 @@ class TYTD.Downloader {
RequestHeaders = [
{
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")
};
this.RateLimit();
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;
/*this.Muxex.Lock();
/*this.Mutex.Lock();
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)});";
Sqlite.Exec(db, d);
@@ -1686,7 +1938,7 @@ class TYTD.Downloader {
};
*/
if(first) {
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
if(isPlaylist)
{
@@ -1722,7 +1974,7 @@ class TYTD.Downloader {
ids.Add(vid);
if(TypeOf(dbRow) == "Long")
{
this.Muxex.Lock();
this.Mutex.Lock();
var db = this.OpenDB();
Sqlite.Exec(db,$"INSERT INTO playlist_entries (playlistId,videoId) VALUES ({dbRow},{Sqlite.Escape(vid)});");
Sqlite.Close(db);
@@ -1743,21 +1995,21 @@ class TYTD.Downloader {
private requests = 0;
private rlm=new Muxex();
private rlm=new Mutex();
private RateLimit()
{
this.rlm.Lock();
var curRequest = DateTime.NowEpoch;
if((curRequest - this.lastRequest) > 60)
if((curRequest - this.lastRequest) > 10)
{
this.requests = 0;
}
this.requests++;
if(this.requests > 5)
if(this.requests >= 1)
{
DateTime.Sleep(25000);
DateTime.Sleep(1500);
this.requests=0;
curRequest = DateTime.NowEpoch;
}
@@ -1798,7 +2050,6 @@ class TYTD.Downloader {
const token = GetSessionToken(ctx);
if(TypeIsString(token))
{
this.Mutex.Lock();
const db = this.OpenDB();
Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(token)};");
@@ -1831,7 +2082,17 @@ class TYTD.Downloader {
}
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)
{
this.Mutex.Lock();
@@ -1852,13 +2113,32 @@ class TYTD.Downloader {
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)};");
if(TypeIsList(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);
this.Mutex.Unlock();
return ParseLong(item.flags) | 1;
}
}
@@ -1867,9 +2147,27 @@ class TYTD.Downloader {
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)
{
his.Mutex.Lock();
this.Mutex.Lock();
const db = this.OpenDB();
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
var noAccounts=true;
@@ -1891,6 +2189,20 @@ class TYTD.Downloader {
if(TypeIsList(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);
this.Mutex.Unlock();
item.flags = ParseLong(item.flags);
@@ -1905,9 +2217,9 @@ class TYTD.Downloader {
public Passwd(ctx, oldPassword, newPassword, logout)
{
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 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();
const db = this.OpenDB();
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))
{
each(var item : user)
{
this.Mutex.Unlock();
var salt = Crypto.Base64Decode(item.password_salt);
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
@@ -1956,18 +2296,22 @@ class TYTD.Downloader {
if(item.password_hash == hashStr)
{
var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025")));
this.Mutex.Lock();
const dbCon = this.OpenDB();
Sqlite.Exec(dbCon, $"INSERT INTO sessions (accountId,key) VALUES ({item.id},{Sqlite.Escape(rand)});");
Sqlite.Close(dbCon);
const expires = doesExpire ? ((DateTime.NowEpoch??0) + UserFlags.Expires) : 0;
Sqlite.Exec(db, $"INSERT INTO sessions (accountId,key,expires) VALUES ({item.id},{Sqlite.Escape(rand)},{expires});");
Sqlite.Close(db);
this.Mutex.Unlock();
return rand;
}
Sqlite.Close(db);
this.Mutex.Unlock();
return null;
}
}
Sqlite.Close(db);
this.Mutex.Unlock();
return null;
}
@@ -1992,6 +2336,25 @@ class TYTD.Downloader {
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"};
}
}

View 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}";
}
}

View File

@@ -27,6 +27,7 @@ func TYTD.GetVideoId(v)
return null;
}
func TYTD.GetPlaylistId(pid)
{
func IsValidId(v)
@@ -84,4 +85,49 @@ func TYTD.GetChannelId(cid)
}
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;
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -20,9 +20,17 @@ class TYTD.SDVideoDownload : IVideoDownload {
this.tytd = tytd;
var id = this.info.VideoId;
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;
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;

View File

@@ -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();
}
}

View File

@@ -9,13 +9,24 @@ class TYTD.TranscodeAudio : IVideoDownload {
{
this.id = id;
this.ext = ext;
this.ncv = new TYTD.AOVideoDownload(id);
this.ncv = new TYTD.SDVideoDownload(id);
}
public setTYTD(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)
@@ -31,15 +42,15 @@ class TYTD.TranscodeAudio : IVideoDownload {
public Start()
{
var id = this.id;
this.mcv.Start();
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 / "ao.bin";
var ao = dir / "ytmux.mp4";
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()];

View File

@@ -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();
}
}

View File

@@ -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
View 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
View File

@@ -0,0 +1,4 @@
# Changelog
## 1.0.0
Make it a little faster, queue is in sqlite now, fix db bug and more

View File

@@ -1,6 +1,6 @@
services:
tytd:
image: onedev.site.tesses.net/tytd2025/tytd2025:latest
image: git.tesses.org/tesses50/tytd2025:latest
volumes:
- tytd-data:/data
ports:

43
nfpm.yaml Normal file
View 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
View 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
View File

@@ -0,0 +1,3 @@
if command -v deb-systemd-helper >/dev/null 2>&1; then
deb-systemd-helper disable tytd2025
fi

View 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
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env sh
exec crosslang vm "/opt/tytd2025/tytd2025.crvm" --port=3255