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

|
||||
|
||||
[Website](https://crosslang.tesseslanguage.com/software/webapps/tytd2025/)
|
||||
|
||||
# Features
|
||||
- PWA with the ability to add videos when server is down
|
||||
- Can download videos, playlists and channels (you need a channel url like this [https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A](https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A))
|
||||
- Can subscribe to channels
|
||||
- Can create playlists (that are stored on the server)
|
||||
- Search and browse your downloaded videos, playlists, or channels (the search is very basic though)
|
||||
- User accounts
|
||||
- Videos can be tagged based on your downloader's TYTD tag (to determine which instance downloaded it)
|
||||
- Plugins from [CPKG](https://cpkg.tesseslanguage.com/) or any CPKG compliant server
|
||||
- Can download YouTube videos either Low quality (but doesn't) require [ffmpeg](https://ffmpeg.org/), MP3, FLAC or AMV (these do need [ffmpeg](https://ffmpeg.org/) in your PATH however)
|
||||
- Runs on the Wii using the [Wii Linux Continuation Project](https://wiibrew.org/wiki/Wii-Linux#Wii_Linux_Continuation_Project) (albeit extremely slowly, despite this that's where I run it)
|
||||
|
||||
# What this project uses (attribution)
|
||||
I don't feel like storing their licenses in my project, so I link to their projects instead
|
||||
|
||||
- [SQLite3](https://www.sqlite.org/) for it's database (embedded into TessesFramework)
|
||||
- [BeerCSS](https://www.beercss.com/) for its webui, licensed under MIT
|
||||
- [HTMX](https://htmx.org/) for the SPA experience
|
||||
- [FFmpeg](https://ffmpeg.org/) if you convert the videos, uses the cli
|
||||
- [YouTubeExplode](https://github.com/Tyrrrz/YoutubeExplode) for some of its json payloads
|
||||
- [NewPipe](https://newpipe.net/) for some of its json payloads
|
||||
- [Material Symbols Font](https://fonts.google.com/icons) (wget from beercss css files)
|
||||
- [VideoJS](https://videojs.org/) for video player
|
||||
- [CrossLang](https://crosslang.tesseslanguage.com/) My programming language
|
||||
|
||||
|
||||
## To Install
|
||||
Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
|
||||
|
||||
from source:
|
||||
```bash
|
||||
cd Tesses.YouTubeDownloader.Server
|
||||
git clone https://git.tesses.org/tesses50/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
|
||||
```
|
||||
|
||||
|
||||
@@ -28,6 +28,6 @@
|
||||
"template_icon": "icon.png"
|
||||
},
|
||||
"name": "Tesses.YouTubeDownloader.PluginTemplate",
|
||||
"version": "1.0.0.0-prod",
|
||||
"version": "1.0.1.0-dev",
|
||||
"icon": "icon.png"
|
||||
}
|
||||
@@ -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.1.0-dev",
|
||||
"compTime": "secure"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<h1>Cannot connect to the TYTD2025 server.</h1>
|
||||
Refresh page to add to offline queue
|
||||
</div>
|
||||
110
Tesses.YouTubeDownloader.Server/res/offline.html
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TYTD2025 is Offline</title>
|
||||
<link rel="stylesheet" href="/beer.min.css">
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<script src="/offline.js" defer></script>
|
||||
<script type="module" src="/beer.min.js" defer></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<a href="./" class="button extend circle">
|
||||
<i>home</i>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<div class="row">
|
||||
<div class="max"></div>
|
||||
<div class="min">
|
||||
<img src="tytd-128.png" alt="tytd-logo">
|
||||
</div>
|
||||
<div class="max"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="max"></div>
|
||||
<div class="min">
|
||||
<h3>Cannot connect to the TYTD2025 server.</h3>
|
||||
</div>
|
||||
<div class="max"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="max"></div>
|
||||
<div class="min">
|
||||
<form>
|
||||
|
||||
<div class="field label border small fill">
|
||||
<input id="url" type="text">
|
||||
<label>Url or id</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="max">
|
||||
<div class="field label suffix border fill">
|
||||
<select id="res">
|
||||
|
||||
|
||||
<option value="NoDownload">Don't Download</option>
|
||||
|
||||
|
||||
|
||||
<option value="LowVideo" selected="">Low (muxed by YouTube)</option>
|
||||
|
||||
|
||||
|
||||
<option value="VideoOnly">Highest video (no audio)</option>
|
||||
|
||||
|
||||
|
||||
<option value="AudioOnly">Highest audio (no video)</option>
|
||||
|
||||
|
||||
|
||||
<option value="MP3">Convert to MP3</option>
|
||||
|
||||
|
||||
|
||||
<option value="FLAC">Convert to FLAC</option>
|
||||
|
||||
|
||||
|
||||
<option value="MP4">Convert to MP4</option>
|
||||
|
||||
|
||||
|
||||
<option value="MKV">Mux to MKV (no transcoding)</option>
|
||||
|
||||
|
||||
|
||||
<option value="DontConvert">Don't convert or Mux</option>
|
||||
|
||||
|
||||
</select>
|
||||
<label>Resolution</label>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min">
|
||||
<button id="btn">
|
||||
<i>add</i>
|
||||
Add when online
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="max"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
btn.onclick = (evt)=>{
|
||||
evt.preventDefault();
|
||||
addOffline(url.value, res.value);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
54
Tesses.YouTubeDownloader.Server/res/offline.js
Normal file
@@ -0,0 +1,54 @@
|
||||
function addOffline(url,res)
|
||||
{
|
||||
var videos = JSON.parse(localStorage.getItem("videos") ?? "[]");
|
||||
videos.push({
|
||||
url: url,
|
||||
res: res
|
||||
});
|
||||
localStorage.setItem('videos',JSON.stringify(videos));
|
||||
}
|
||||
|
||||
async function syncOffline()
|
||||
{
|
||||
const json = localStorage.getItem("videos") ?? "[]";
|
||||
if(json !== "[]")
|
||||
{
|
||||
const resp = await fetch('/api/v1/add',{
|
||||
body: json,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "POST"
|
||||
});
|
||||
if(resp.ok)
|
||||
{
|
||||
localStorage.removeItem('videos');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(navigator.online)
|
||||
{
|
||||
syncOffline();
|
||||
}
|
||||
|
||||
window.addEventListener('online',()=>{
|
||||
syncOffline();
|
||||
});
|
||||
|
||||
async function getPersonalTempLink()
|
||||
{
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const ent = searchParams.get("name");
|
||||
if(ent)
|
||||
{
|
||||
const resp=await fetch(`./api/v1/personal_tmp_link?name=${encodeURIComponent(ent)}`);
|
||||
if(resp.ok)
|
||||
{
|
||||
const text = await resp.text();
|
||||
the_playlist_url.innerText = text;
|
||||
the_playlist_url.href = text;
|
||||
ui("#dialog");
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Tesses.YouTubeDownloader.Server/res/service_worker.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const assets = ["<@ASSETS@>"];
|
||||
|
||||
const staticCacheName = "tytd-static-<@BUILD_TIME@>";
|
||||
|
||||
async function ThrowOnBadGateway(req)
|
||||
{
|
||||
const resp = await fetch(req);
|
||||
if(resp.status === 502) throw new Error("Bad Gateway");
|
||||
return resp;
|
||||
}
|
||||
|
||||
self.addEventListener('install',evt => {
|
||||
evt.waitUntil(
|
||||
caches.open(staticCacheName).then(cache =>{
|
||||
cache.addAll(assets);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate',(evt)=>{
|
||||
evt.waitUntil(
|
||||
caches.keys().then(keys => {
|
||||
return Promise.all(keys.filter(key => key !== staticCacheName).map(key => caches.delete(key)));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', evt => {
|
||||
|
||||
evt.respondWith(
|
||||
|
||||
caches.match(evt.request).then(cacheRes=>{
|
||||
console.warn('SW cacheRes:', cacheRes);
|
||||
return cacheRes || ThrowOnBadGateway(evt.request);
|
||||
}).catch((err)=>{
|
||||
console.warn('SW Fallback:', err.message);
|
||||
if(evt.request.headers.has('HX-Request'))
|
||||
{
|
||||
|
||||
return new Response("",{
|
||||
status: 200,
|
||||
headers: {
|
||||
'HX-Redirect': "/offline.html"
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return caches.match('/offline.html');
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
73
Tesses.YouTubeDownloader.Server/res/site.webmanifest
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"short_name": "TYTD2025",
|
||||
"name": "Tesses YouTubeDownloader 2025",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#ffb2be",
|
||||
"background_color": "#241e1f",
|
||||
"description": "A web based YouTube archiver to preserve videos that might be removed",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/tytd.svg",
|
||||
"sizes": "192x192 256x256 384x384 512x512",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/tytd-128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/tytd-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/tytd-256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/tytd-384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/tytd-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/tytd-1024.png",
|
||||
"sizes": "1024x1024",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Open Downloads",
|
||||
"short_name": "Downloads",
|
||||
"description": "Open Downloads page",
|
||||
"url": "/downloads"
|
||||
},
|
||||
{
|
||||
"name": "Open Installed Plugins",
|
||||
"short_name": "Installed plugins",
|
||||
"description": "Open the installed plugins page",
|
||||
"url": "/plugins"
|
||||
},
|
||||
{
|
||||
"name": "Open Download Plugins",
|
||||
"short_name": "Download plugins",
|
||||
"description": "Open the download plugins page",
|
||||
"url": "/plugins-download"
|
||||
},
|
||||
{
|
||||
"name": "Open Settings",
|
||||
"short_name": "Settings",
|
||||
"description": "Open settings page",
|
||||
"url": "/settings"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Tesses.YouTubeDownloader.Server/res/tytd-1024.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-192.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-256.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-384.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-512.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
@@ -1,62 +0,0 @@
|
||||
func Components.DiscoverEntry(item)
|
||||
{
|
||||
switch(item.type)
|
||||
{
|
||||
case "video":
|
||||
return
|
||||
<div class="row">
|
||||
<div class="min">
|
||||
<img src={$"./video-thumbnail?v={Net.Http.UrlEncode(item.id)}"} width="144" width="120">
|
||||
</div>
|
||||
<div class="max">
|
||||
<div class="col">
|
||||
<div class="min">
|
||||
<a target="_blank" href={$"./watch?v={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
|
||||
</div>
|
||||
<div class="min">
|
||||
<span>{item.views} • {item.uploaded}</span>
|
||||
</div>
|
||||
<div class="min">
|
||||
<each(var part : item.author)>
|
||||
<if(TypeOf(part.navigationEndpoint) == "Dictionary")>
|
||||
<true>
|
||||
<a target="_blank" href={$"./channel?id={Net.Http.UrlEncode(part.navigationEndpoint.browseEndpoint.browseId)}"}>{part.text}</a>
|
||||
|
||||
</true>
|
||||
<false>
|
||||
<span>{part.text}</span>
|
||||
</false>
|
||||
</if>
|
||||
</each>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
break;
|
||||
case "channel":
|
||||
return
|
||||
<div class="row">
|
||||
<div class="min">
|
||||
<img src={$"./channel-thumbnail?id={Net.Http.UrlEncode(item.id)}"} width="144" width="144">
|
||||
</div>
|
||||
<div class="max">
|
||||
<div class="col">
|
||||
<div class="min">
|
||||
<a target="_blank" href={$"./channel?id={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
|
||||
</div>
|
||||
<div class="min">
|
||||
<span>{item.subs}</span>
|
||||
</div>
|
||||
<div class ="min">
|
||||
<p class="min">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
break;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
func Components.MusicAlbum(item)
|
||||
{
|
||||
return <div class="row">
|
||||
<div class="min">
|
||||
<img src={$"./album-art?id={Net.Http.UrlEncode(item.id)}"} width="200" height="200">
|
||||
</div>
|
||||
<div class="min">
|
||||
<a href={$"./music-album?id={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
func Components.MusicArtist(item)
|
||||
{
|
||||
return <a href={$"./music-artist?id={Net.Http.UrlEncode(item.id)}"}>{item.name}</a>;
|
||||
}
|
||||
@@ -20,11 +20,19 @@ func Components.PersonalListDescription(tytd,name,editing)
|
||||
</form>
|
||||
</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>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
var progress=0;
|
||||
|
||||
func Components.Progress(tytd)
|
||||
{
|
||||
var vid = tytd.CurrentVideo;
|
||||
var progress = tytd.GetProgress();
|
||||
|
||||
var html = <div hx-trigger="every 1500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
|
||||
<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>
|
||||
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(progress.CurrentVideo.VideoId)}"} href={$"./watch?v={Net.Http.UrlEncode(progress.CurrentVideo.VideoId)}"}>{progress.CurrentVideo.Title}</a></h2>
|
||||
<h4><a hx-target="body" hx-push-url="true" hx-get={$"./channel?id={Net.Http.UrlEncode(progress.CurrentVideo.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(progress.CurrentVideo.ChannelId)}"}>{progress.CurrentVideo.Channel}</a></h4>
|
||||
<progress class="wavy light-green-text" value={progress.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
|
||||
</div>;
|
||||
progress++;
|
||||
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)
|
||||
{
|
||||
|
||||
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>;
|
||||
}
|
||||
@@ -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({
|
||||
@@ -68,16 +139,19 @@ class TYTDApp {
|
||||
{
|
||||
var incorrect=false;
|
||||
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
||||
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
|
||||
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
||||
const username = ctx.BodyParams.TryGetFirst("username") ?? "";
|
||||
const password = ctx.BodyParams.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;
|
||||
@@ -121,16 +218,16 @@ class TYTDApp {
|
||||
{
|
||||
if(ctx.Method == "POST")
|
||||
{
|
||||
const from = ctx.QueryParams.TryGetFirstInt("from");
|
||||
const from = ctx.BodyParams.TryGetFirstInt("from");
|
||||
switch(from)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
const enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins");
|
||||
const tag = ctx.QueryParams.TryGetFirst("tag") ?? "UnknownPC";
|
||||
const pollHours = ctx.QueryParams.TryGetFirstInt("pollHours") ?? 3;
|
||||
const pollMinutes = ctx.QueryParams.TryGetFirstInt("pollMinutes") ?? 0;
|
||||
const pollSeconds = ctx.QueryParams.TryGetFirstInt("pollSeconds") ?? 0;
|
||||
const enablePlugins = ctx.BodyParams.GetFirstBoolean("enablePlugins");
|
||||
const tag = ctx.BodyParams.TryGetFirst("tag") ?? "UnknownPC";
|
||||
const pollHours = ctx.BodyParams.TryGetFirstInt("pollHours") ?? 3;
|
||||
const pollMinutes = ctx.BodyParams.TryGetFirstInt("pollMinutes") ?? 0;
|
||||
const pollSeconds = ctx.BodyParams.TryGetFirstInt("pollSeconds") ?? 0;
|
||||
|
||||
|
||||
this.OOBE_STATE.tag = tag;
|
||||
@@ -151,7 +248,7 @@ class TYTDApp {
|
||||
case 3:
|
||||
{
|
||||
|
||||
const createUser = ctx.QueryParams.GetFirstBoolean("user");
|
||||
const createUser = (ctx.BodyParams.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,39 +341,98 @@ 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;
|
||||
var result = {success=true};
|
||||
|
||||
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
||||
const newpassword = ctx.QueryParams.TryGetFirst("newpassword") ?? "";
|
||||
|
||||
const confirm = ctx.QueryParams.TryGetFirst("newpassword") ?? "";
|
||||
const redirect = ctx.BodyParams.TryGetFirst("redirect") ?? "/login";
|
||||
const password = ctx.BodyParams.TryGetFirst("password") ?? "";
|
||||
const newpassword = ctx.BodyParams.TryGetFirst("newpassword") ?? "";
|
||||
|
||||
const confirm = ctx.BodyParams.TryGetFirst("confirm") ?? "";
|
||||
const logout = ctx.BodyParams.GetFirstBoolean("logout");
|
||||
if(ctx.Method == "POST")
|
||||
{
|
||||
this.TYTD.Passwd();
|
||||
//public Passwd(ctx, oldPassword, newPassword, logout)
|
||||
if(password != newpassword)
|
||||
{
|
||||
result = {success = false, reason = "Passwords do not match"};
|
||||
}
|
||||
else
|
||||
result = this.TYTD.Passwd(ctx, password, newpassword, logout);
|
||||
|
||||
if(result.success)
|
||||
{
|
||||
ctx.StatusCode = 307;
|
||||
ctx.SendRedirect(redirect);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
|
||||
|
||||
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, result.success ? null : result.reason));
|
||||
return true;
|
||||
}
|
||||
|
||||
else if(ctx.Path == "/newuser")
|
||||
{
|
||||
var error = null;
|
||||
if(ctx.Method == "POST")
|
||||
{
|
||||
const username = ctx.QueryParams.TryGetFirst("username");
|
||||
const password = ctx.QueryParams.TryGetFirst("password");
|
||||
const confirm = ctx.QueryParams.TryGetFirst("confirm");
|
||||
const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin");
|
||||
const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins");
|
||||
const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins");
|
||||
const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB");
|
||||
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
||||
const username = ctx.BodyParams.TryGetFirst("username");
|
||||
const password = ctx.BodyParams.TryGetFirst("password");
|
||||
const confirm = ctx.BodyParams.TryGetFirst("confirm");
|
||||
const isAdmin = ctx.BodyParams.GetFirstBoolean("isAdmin");
|
||||
const canUsePlugins = ctx.BodyParams.GetFirstBoolean("canUsePlugins");
|
||||
const canManagePlugins = ctx.BodyParams.GetFirstBoolean("canManagePlugins");
|
||||
const canDownloadDB = ctx.BodyParams.GetFirstBoolean("canDownloadDB");
|
||||
|
||||
var flags = 0;
|
||||
if(isAdmin) flags |= UserFlags.AdminFlag;
|
||||
if(canUsePlugins) flags |= UserFlags.PluginFlag;
|
||||
@@ -299,12 +454,6 @@ class TYTDApp {
|
||||
else {
|
||||
error = this.TYTD.CreateAccount(ctx, username, password, flags);
|
||||
}
|
||||
|
||||
if(!TypeIsString(error))
|
||||
{
|
||||
ctx.StatusCode=303;
|
||||
ctx.SendRedirect(redirect);
|
||||
}
|
||||
}
|
||||
ctx.WithMimeType("text/html").SendText(Pages.NewUser(error));
|
||||
return true;
|
||||
@@ -340,6 +489,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");
|
||||
@@ -361,16 +540,30 @@ class TYTDApp {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else if(ctx.Path == "/api/v1/progress_ex.json")
|
||||
{
|
||||
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetProgressEx());
|
||||
return true;
|
||||
}
|
||||
else if(ctx.Path == "/api/v1/progress.json")
|
||||
{
|
||||
var jo = {
|
||||
CurrentVideoProgress = this.TYTD.CurrentVideoProgress,
|
||||
CurrentVideo = this.TYTD.CurrentVideo
|
||||
|
||||
};
|
||||
ctx.WithMimeType("application/json").SendJson(jo);
|
||||
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetProgress());
|
||||
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 +593,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 +643,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")
|
||||
{
|
||||
/*
|
||||
@@ -572,13 +804,31 @@ class TYTDApp {
|
||||
}
|
||||
else if(ctx.Method == "POST")
|
||||
{
|
||||
var description =ctx.QueryParams.TryGetFirst("description");
|
||||
var description =ctx.BodyParams.TryGetFirst("description");
|
||||
this.TYTD.SetPersonalListDescription(name,description);
|
||||
ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,false));
|
||||
|
||||
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 +849,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");
|
||||
@@ -612,10 +862,10 @@ class TYTDApp {
|
||||
{
|
||||
if(whoami.username != user)
|
||||
{
|
||||
const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin");
|
||||
const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins");
|
||||
const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins");
|
||||
const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB");
|
||||
const isAdmin = ctx.BodyParams.GetFirstBoolean("isAdmin");
|
||||
const canUsePlugins = ctx.BodyParams.GetFirstBoolean("canUsePlugins");
|
||||
const canManagePlugins = ctx.BodyParams.GetFirstBoolean("canManagePlugins");
|
||||
const canDownloadDB = ctx.BodyParams.GetFirstBoolean("canDownloadDB");
|
||||
var flags = 0;
|
||||
if(isAdmin) flags |= UserFlags.AdminFlag;
|
||||
if(canUsePlugins) flags |= UserFlags.PluginFlag;
|
||||
@@ -677,7 +927,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")
|
||||
@@ -690,11 +940,11 @@ class TYTDApp {
|
||||
tag
|
||||
enablePlugins
|
||||
*/
|
||||
var enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins");
|
||||
var tag = ctx.QueryParams.TryGetFirst("tag");
|
||||
var hours = ctx.QueryParams.TryGetFirstInt("hours");
|
||||
var minutes = ctx.QueryParams.TryGetFirstInt("minutes");
|
||||
var seconds = ctx.QueryParams.TryGetFirstInt("seconds");
|
||||
var enablePlugins = ctx.BodyParams.GetFirstBoolean("enablePlugins");
|
||||
var tag = ctx.BodyParams.TryGetFirst("tag");
|
||||
var hours = ctx.BodyParams.TryGetFirstInt("hours");
|
||||
var minutes = ctx.BodyParams.TryGetFirstInt("minutes");
|
||||
var seconds = ctx.BodyParams.TryGetFirstInt("seconds");
|
||||
|
||||
if(TypeOf(tag) == "String" && TypeOf(hours) == "Long" && TypeOf(minutes) == "Long" && TypeOf(seconds) == "Long")
|
||||
{
|
||||
@@ -714,6 +964,44 @@ class TYTDApp {
|
||||
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
|
||||
return true;
|
||||
}
|
||||
|
||||
else if(ctx.Path == "/watch_videos")
|
||||
{
|
||||
|
||||
if(ctx.Method=="GET")
|
||||
{
|
||||
const video_ids = ctx.QueryParams.TryGetFirst("video_ids");
|
||||
|
||||
if(TypeIsString(video_ids))
|
||||
{
|
||||
ctx.WithMimeType("text/html").SendText(Pages.YouTubeAnonyPlaylist(video_ids));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(ctx.Method=="POST")
|
||||
{
|
||||
const video_ids = ctx.BodyParams.TryGetFirst("video_ids");
|
||||
|
||||
if(TypeIsString(video_ids))
|
||||
{
|
||||
const name = ctx.BodyParams.TryGetFirst("name");
|
||||
if(TypeIsString(name))
|
||||
{
|
||||
const nameParts=video_ids.Split(",");
|
||||
//Console.WriteLine(nameParts);
|
||||
each(var item : nameParts)
|
||||
{
|
||||
|
||||
this.TYTD.AddToPersonalList(name,item);
|
||||
}
|
||||
//Console.WriteLine(name);
|
||||
ctx.SendRedirect($"/list?name={Net.Http.UrlEncode(name)}",303);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if(ctx.Path == "/playlist")
|
||||
{
|
||||
ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx));
|
||||
@@ -726,9 +1014,9 @@ class TYTDApp {
|
||||
}
|
||||
else if(ctx.Path == "/add")
|
||||
{
|
||||
var url = ctx.QueryParams.TryGetFirst("url");
|
||||
var action =ctx.QueryParams.TryGetFirst("action");
|
||||
var res = ctx.QueryParams.TryGetFirst("res");
|
||||
var url = ctx.BodyParams.TryGetFirst("url");
|
||||
var action =ctx.BodyParams.TryGetFirst("action");
|
||||
var res = ctx.BodyParams.TryGetFirst("res");
|
||||
if(action == "add")
|
||||
{
|
||||
this.TYTD.DownloadItem(url,res);
|
||||
@@ -756,9 +1044,9 @@ class TYTDApp {
|
||||
}
|
||||
else if(ctx.Path == "/add-video")
|
||||
{
|
||||
var url = ctx.QueryParams.TryGetFirst("url");
|
||||
var action =ctx.QueryParams.TryGetFirst("action");
|
||||
var res = ctx.QueryParams.TryGetFirst("res");
|
||||
var url = ctx.BodyParams.TryGetFirst("url");
|
||||
var action =ctx.BodyParams.TryGetFirst("action");
|
||||
var res = ctx.BodyParams.TryGetFirst("res");
|
||||
if(action == "add")
|
||||
{
|
||||
this.TYTD.DownloadItem(url,res);
|
||||
@@ -779,10 +1067,10 @@ class TYTDApp {
|
||||
}
|
||||
else if(ctx.Path == "/add-to-list")
|
||||
{
|
||||
var id = ctx.QueryParams.TryGetFirst("id");
|
||||
var id = ctx.BodyParams.TryGetFirst("id");
|
||||
if(TypeOf(id) != "String") id = "";
|
||||
|
||||
var name = ctx.QueryParams.TryGetFirst("name");
|
||||
var name = ctx.BodyParams.TryGetFirst("name");
|
||||
if(TypeOf(name) != "String") name = "";
|
||||
|
||||
if(name == "")
|
||||
@@ -802,6 +1090,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));
|
||||
@@ -817,9 +1111,9 @@ class TYTDApp {
|
||||
{
|
||||
if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false;
|
||||
var data = {
|
||||
name = ctx.QueryParams.TryGetFirst("name"),
|
||||
version = ctx.QueryParams.TryGetFirst("version"),
|
||||
action = ctx.QueryParams.TryGetFirst("action")
|
||||
name = ctx.BodyParams.TryGetFirst("name"),
|
||||
version = ctx.BodyParams.TryGetFirst("version"),
|
||||
action = ctx.BodyParams.TryGetFirst("action")
|
||||
};
|
||||
|
||||
var mustConfirm = false;
|
||||
|
||||
@@ -72,5 +72,5 @@ func Pages.Admin(tytd,ctx)
|
||||
|
||||
</form>;
|
||||
|
||||
return Components.Shell("Admin Settings",html ,3);
|
||||
return Components.Shell("Admin Settings",html ,4);
|
||||
}
|
||||
@@ -8,5 +8,5 @@ func Pages.Api()
|
||||
</a>
|
||||
</ul>
|
||||
</null>;
|
||||
return Components.Shell("Api",html,4);
|
||||
return Components.Shell("Api",html,5);
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
func Pages.ChangePassword(redirect,incorrect)
|
||||
func Pages.ChangePassword(redirect,error)
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
const html = <null>
|
||||
<if(incorrect)>
|
||||
<if(TypeIsString(error))>
|
||||
<true>
|
||||
<blockquote>
|
||||
<h5>The password could not be changed</h5>
|
||||
<h5>{error}</h5>
|
||||
</blockquote>
|
||||
</true>
|
||||
</if>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
</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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
</null>;
|
||||
|
||||
return Components.Shell("New User",html ,3);
|
||||
return Components.Shell("New User",html ,4);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func Pages.OobePage2(tytd,ctx)
|
||||
<div class="small-margin">Security</div>
|
||||
</div>
|
||||
</nav>
|
||||
<form hx-get="./oobe" hx-target="body" hx-push-url="true">
|
||||
<form hx-post="./oobe" hx-target="body" hx-push-url="true">
|
||||
<input type="hidden" name="page" value="2">
|
||||
<div class="field suffix border round">
|
||||
<select name="server">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
return Components.Shell($"Playlist {title}",html,1);
|
||||
return Components.Shell($"Playlist {title}",html,2);
|
||||
}
|
||||
@@ -75,5 +75,5 @@ func Pages.DownloadPlugins(tytd,ctx)
|
||||
|
||||
</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>
|
||||
</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>;
|
||||
|
||||
return Components.Shell("Settings",html ,3);
|
||||
return Components.Shell("Settings",html ,4);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -14,6 +14,6 @@
|
||||
}
|
||||
],
|
||||
"name": "Tesses.YouTubeDownloader",
|
||||
"version": "1.0.0.0-dev",
|
||||
"version": "1.0.1.0-dev",
|
||||
"icon": "icon.png"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
/^ 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"}
|
||||
];
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
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;
|
||||
|
||||
@@ -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.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()];
|
||||
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
class TYTD.TranscodeVideo : IVideoDownload {
|
||||
private id;
|
||||
private ncv;
|
||||
private tytd;
|
||||
private ext;
|
||||
|
||||
public TranscodeVideo(id,ext)
|
||||
{
|
||||
this.id = id;
|
||||
this.ext = ext;
|
||||
this.ncv = new TYTD.NoConvertVideoDownload(id);
|
||||
}
|
||||
|
||||
public setTYTD(tytd)
|
||||
{
|
||||
this.tytd = tytd;
|
||||
return this.ncv.TYTD = tytd;
|
||||
}
|
||||
|
||||
public setProgress(p)
|
||||
{
|
||||
return this.ncv.Progress = p;
|
||||
}
|
||||
|
||||
public getVideo()
|
||||
{
|
||||
return this.ncv.Video;
|
||||
}
|
||||
|
||||
public Start()
|
||||
{
|
||||
var id = this.id;
|
||||
this.mcv.Start();
|
||||
|
||||
var p = new Process();
|
||||
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
||||
|
||||
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
|
||||
var vo = dir / "vo.bin";
|
||||
var ao = dir / "ao.bin";
|
||||
var out = dir / $"conv{this.ext}";
|
||||
if(FS.Local.FileExists(out)) return;
|
||||
var args=["-y","-i",vo.ToString(),"-i",ao.ToString()];
|
||||
|
||||
if(this.ext == ".mkv")
|
||||
{
|
||||
args.Add("-c");
|
||||
args.Add("copy");
|
||||
}
|
||||
args.Add("-map");
|
||||
args.Add("0:v");
|
||||
args.Add("-map");
|
||||
args.Add("1:a");
|
||||
args.Add("-preset");
|
||||
args.Add("ultrafast");
|
||||
args.Add(out.ToString());
|
||||
|
||||
|
||||
p.Arguments = args;
|
||||
if(p.Start())
|
||||
p.Join();
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
class TYTD.VOVideoDownload : IVideoDownload {
|
||||
private info;
|
||||
private tytd;
|
||||
private progress;
|
||||
private video_stream_url;
|
||||
|
||||
private done=false;
|
||||
|
||||
public VOVideoDownload(id)
|
||||
{
|
||||
this.info = {
|
||||
Title = "",
|
||||
Channel = "",
|
||||
VideoId = TYTD.GetVideoId(id),
|
||||
ChannelId = ""
|
||||
};
|
||||
}
|
||||
public setTYTD(tytd)
|
||||
{
|
||||
this.tytd = tytd;
|
||||
var id = this.info.VideoId;
|
||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
|
||||
if(this.tytd.Storage.FileExists(path)) {
|
||||
this.done = true;
|
||||
return tytd;
|
||||
}
|
||||
|
||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
||||
this.info.Title = req.videoDetails.title;
|
||||
this.info.Channel = req.videoDetails.author;
|
||||
this.info.ChannelId = req.videoDetails.channelId;
|
||||
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video");
|
||||
Console.WriteLine($"Downloading: {this.info.Title} with id: {id} Highest Video");
|
||||
|
||||
this.tytd.PutVideoInfo(req.videoDetails);
|
||||
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var url = "";
|
||||
|
||||
each(var item : req.streamingData.adaptiveFormats)
|
||||
{
|
||||
if(TypeOf(item.height) == "Long")
|
||||
{
|
||||
if(item.width >= width && item.height >= height)
|
||||
{
|
||||
url = item.url;
|
||||
width = item.width;
|
||||
height = item.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.video_stream_url = url;
|
||||
|
||||
return tytd;
|
||||
}
|
||||
public setProgress(p)
|
||||
{
|
||||
this.progress = p;
|
||||
}
|
||||
public getVideo()
|
||||
{
|
||||
return this.info;
|
||||
}
|
||||
public Start()
|
||||
{
|
||||
if(this.done) return;
|
||||
var req = {
|
||||
FollowRedirects = true,
|
||||
RequestHeaders = [
|
||||
{
|
||||
Key = "User-Agent",
|
||||
|
||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
||||
}
|
||||
]
|
||||
};
|
||||
var resp = Net.Http.MakeRequest(this.video_stream_url,req);
|
||||
|
||||
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||
{
|
||||
var id = this.info.VideoId;
|
||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
|
||||
this.tytd.Storage.CreateDirectory(path.GetParent());
|
||||
var strm = this.tytd.Storage.OpenFile(path+".part","wb");
|
||||
var src = resp.ReadAsStream();
|
||||
Helpers.CopyToProgress(src,strm,this.progress,100.0);
|
||||
strm.Close();
|
||||
src.Close();
|
||||
this.tytd.Storage.MoveFile(path+".part",path);
|
||||
}
|
||||
else {
|
||||
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var url = "";
|
||||
|
||||
each(var item : req.streamingData.adaptiveFormats)
|
||||
{
|
||||
if(TypeOf(item.height) == "Long")
|
||||
{
|
||||
if(item.width >= width && item.height >= height)
|
||||
{
|
||||
url = item.url;
|
||||
width = item.width;
|
||||
height = item.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.video_stream_url = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
build-for-system-pkg.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
cd Tesses.YouTubeDownloader.Server
|
||||
crosslang build
|
||||
cd bin
|
||||
cp Tesses.YouTubeDownloader.Server-*.crvm tytd2025.crvm
|
||||
cd ../..
|
||||
mkdir -p packages-tmp
|
||||
nfpm package -t packages-tmp -p archlinux
|
||||
curl --user tesses50:$PACKAGE_AND_BREW \
|
||||
--upload-file packages-tmp/*.pkg.tar.zst \
|
||||
https://git.tesses.org/api/packages/tesses50/arch/core
|
||||
nfpm package -t packages-tmp -p deb
|
||||
curl --user tesses50:$PACKAGE_AND_BREW \
|
||||
--upload-file packages-tmp/*.deb \
|
||||
https://git.tesses.org/api/packages/tesses50/debian/pool/universal/main/upload
|
||||
nfpm package -t packages-tmp -p rpm
|
||||
curl --user tesses50:$PACKAGE_AND_BREW \
|
||||
--upload-file packages-tmp/*.rpm \
|
||||
https://git.tesses.org/api/packages/tesses50/rpm/upload
|
||||
7
changeme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.1
|
||||
Target crosslang v0.0.10, add /api/v1/progress_ex.json
|
||||
|
||||
## 1.0.0
|
||||
Make it a little faster, queue is in sqlite now, fix db bug and more
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
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
@@ -0,0 +1,43 @@
|
||||
# This is an example nfpm configuration file.
|
||||
# Make sure to check the documentation at https://nfpm.goreleaser.com
|
||||
#
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/need to use them.
|
||||
# yaml-language-server: $schema=https://nfpm.goreleaser.com/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
name: "tytd2025"
|
||||
arch: "any"
|
||||
platform: "linux"
|
||||
version: "1.0.1"
|
||||
section: "default"
|
||||
priority: "extra"
|
||||
|
||||
depends:
|
||||
- crosslang
|
||||
recommends:
|
||||
- ffmpeg
|
||||
maintainer: "Mike Nolan <tesses@tesses.net>"
|
||||
description: Tesses YouTube Downloader
|
||||
vendor: "Tesses"
|
||||
homepage: "https://git.tesses.org/tesses50/tytd2025.git"
|
||||
license: "GPLv3"
|
||||
contents:
|
||||
- src: "Tesses.YouTubeDownloader.Server/bin/tytd2025.crvm"
|
||||
dst: "/opt/tytd2025/tytd2025.crvm"
|
||||
- src: "Tesses.YouTubeDownloader.Server/bin/Tesses.YouTubeDownloader-*.crvm"
|
||||
dst: "/opt/tytd2025/"
|
||||
- src: "Tesses.YouTubeDownloader.Server/bin/Tesses.CrossLang.BuildEssentials-*.crvm"
|
||||
dst: "/opt/tytd2025/"
|
||||
- src: package/systemd/
|
||||
dst: /etc/systemd/system/
|
||||
type: tree
|
||||
- src: package/usrbin/
|
||||
dst: /usr/bin/
|
||||
type: tree
|
||||
file_info:
|
||||
mode: 0755
|
||||
|
||||
scripts:
|
||||
postinstall: ./package/postinstall.sh
|
||||
preremove: ./package/preremove.sh
|
||||
5
package/postinstall.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
useradd --system --create-home --shell /usr/sbin/nologin tytd2025 || true
|
||||
|
||||
if command -v deb-systemd-helper >/dev/null 2>&1; then
|
||||
deb-systemd-helper enable tytd2025
|
||||
fi
|
||||
3
package/preremove.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
if command -v deb-systemd-helper >/dev/null 2>&1; then
|
||||
deb-systemd-helper disable tytd2025
|
||||
fi
|
||||
14
package/systemd/tytd2025.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Tesses YouTube Downloader
|
||||
Documentation=https://git.tesses.org/tesses50/tytd2025
|
||||
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/tytd2025
|
||||
Restart=on-failure
|
||||
User=tytd2025
|
||||
StandardOutput=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
2
package/usrbin/tytd2025
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
exec crosslang vm "/opt/tytd2025/tytd2025.crvm" --port=3255
|
||||