commit 5e354c05be2eead43180b1e06c27cc055f461e30 Author: Mike Nolan Date: Wed Oct 15 00:07:35 2025 -0500 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e474a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin +obj +publish +TYTD \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..31bc2f2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM onedev.site.tesses.net/crosslang/crosslangextras/crosslangextras:latest AS build + +WORKDIR /src/ +RUN git clone https://onedev.site.tesses.net/tytd2025 && cd tytd2025/Tesses.YouTubeDownloader.Server && crosslang build + +FROM onedev.site.tesses.net/crosslang/crosslang:latest + +COPY --from=build /src/Tesses.YouTubeDownloader.Server/bin /app +WORKDIR /data + +EXPOSE 3255 + +ENTRYPOINT ["crossvm","--port=3255","/app/Tesses.YouTubeDownloader.Server.crvm"] \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.PluginTemplate/.crossarchiveignore b/Tesses.YouTubeDownloader.PluginTemplate/.crossarchiveignore new file mode 100644 index 0000000..1746e32 --- /dev/null +++ b/Tesses.YouTubeDownloader.PluginTemplate/.crossarchiveignore @@ -0,0 +1,2 @@ +bin +obj diff --git a/Tesses.YouTubeDownloader.PluginTemplate/cross.json b/Tesses.YouTubeDownloader.PluginTemplate/cross.json new file mode 100644 index 0000000..d759cac --- /dev/null +++ b/Tesses.YouTubeDownloader.PluginTemplate/cross.json @@ -0,0 +1,31 @@ +{ + "dependencies": [ + ], + "info": { + "description": "Plugin Template for TYTD2025", + "maintainer": "Mike Nolan", + "repo":"https://onedev.site.tesses.net/tytd2025", + "homepage": "https://tesses.net/apps/tytd/2025/", + "short_name": "tytd2025plugin", + "short_name_pretty": "Tesses YouTube Downloader 2025 Plugin Template", + "license": "GPLv3", + "template_extra_text_ftles": [ + ], + "template_ignored_files": [ + "bin", + "obj" + ], + "template_info": { + "type": "lib", + "plugin_host": "tytd2025", + "short_name": "changeme", + "short_name_pretty": "Change Me", + "license": "GPLv3" + }, + "template_project_dependencies": [ + ], + "type": "template" + }, + "name": "Tesses.YouTubeDownloader.PluginTemplate", + "version": "1.0.0.0-prod" +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.PluginTemplate/src/main.tcross b/Tesses.YouTubeDownloader.PluginTemplate/src/main.tcross new file mode 100644 index 0000000..f9e93b2 --- /dev/null +++ b/Tesses.YouTubeDownloader.PluginTemplate/src/main.tcross @@ -0,0 +1 @@ +func PluginInit() new Plugin(); \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.PluginTemplate/src/plugin.tcross b/Tesses.YouTubeDownloader.PluginTemplate/src/plugin.tcross new file mode 100644 index 0000000..c68f5d4 --- /dev/null +++ b/Tesses.YouTubeDownloader.PluginTemplate/src/plugin.tcross @@ -0,0 +1,29 @@ +/^ + %PROJECT_NAME Class +^/ +class Plugin { + public Plugin() + { + /* + You can access these anywhere in the plugin + + TYTD.GetVideoId(id): get the youtube video id from url or id + TYTD.GetPlaylistId(id): get the youtube playlist id from url or id + TYTD.GetChannelId(id): get the youtube channel id from url or id + TYTD.Config[key]: get a string setting for this plugin + TYTD.Config[key] = value: set a string setting for this plugin (is presistant) + TYTD.Config.Directory: a crosslang SubdirFilesystem for the Files directory in plugin directory + TYTD.Config.DirectoryPath: same directory but the actual path of it for FS.Local + + Resolution, SubscriptionBell, TYTD.Downloader (the instance, you can't create your own instance of it): See https://cpkg.tesseslanguage.com/package_docs?name=Tesses.YoutubeDownloader&version=1.0.0.0-prod + + */ + Console.WriteLine("Run your initialization here"); + } + + public Server = new PluginServer(); + public Close() + { + Console.WriteLine("Do any finishing work"); + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.PluginTemplate/src/server.tcross b/Tesses.YouTubeDownloader.PluginTemplate/src/server.tcross new file mode 100644 index 0000000..9351a4e --- /dev/null +++ b/Tesses.YouTubeDownloader.PluginTemplate/src/server.tcross @@ -0,0 +1,8 @@ +class PluginServer +{ + public Handle(ctx) + { + ctx.WithMimeType().SendText(

Hello, world from @%PROJECT_NAME

Path: {ctx.Path}, OriginalPath: {ctx.OriginalPath}

); + return true; + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/cross.json b/Tesses.YouTubeDownloader.Server/cross.json new file mode 100644 index 0000000..c28631a --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/cross.json @@ -0,0 +1,18 @@ +{ + "icon": "icon.png", + "info": { + "description": "Download YouTube Videos (using CrossLang, via web interface, great for homelabs)", + "maintainer": "Mike Nolan", + "repo":"https://onedev.site.tesses.net/tytd2025", + "homepage": "https://tesses.net/apps/tytd/2025/", + "license": "GPLv3", + "short_name": "tytd2025", + "short_name_pretty": "Tesses YouTube Downloader 2025", + "type": "webapp" + }, + "name": "Tesses.YouTubeDownloader.Server", + "project_dependencies": [ + "..\/Tesses.YouTubeDownloader" + ], + "version": "1.0.0.0-prod" +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/beer.min.css b/Tesses.YouTubeDownloader.Server/res/beer.min.css new file mode 100644 index 0000000..97448dc --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/beer.min.css @@ -0,0 +1 @@ +:root{--size: 1rem;--font: Inter, Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans", Noto Sans, Arial, sans-serif;--font-icon: "Material Symbols Outlined";--speed1: .1s;--speed2: .2s;--speed3: .3s;--speed4: .4s;--active: rgb(128 128 128 / .192);--overlay: rgb(0 0 0 / .5);--elevate1: 0 .125rem .125rem 0 rgb(0 0 0 / .32);--elevate2: 0 .25rem .5rem 0 rgb(0 0 0 / .4);--elevate3: 0 .375rem .75rem 0 rgb(0 0 0 / .48);--top: env(safe-area-inset-top);--bottom: env(safe-area-inset-bottom);--left: env(safe-area-inset-left);--right: env(safe-area-inset-right)}:root,body.light{--primary: #6750a4;--on-primary: #ffffff;--primary-container: #e9ddff;--on-primary-container: #22005d;--secondary: #625b71;--on-secondary: #ffffff;--secondary-container: #e8def8;--on-secondary-container: #1e192b;--tertiary: #7e5260;--on-tertiary: #ffffff;--tertiary-container: #ffd9e3;--on-tertiary-container: #31101d;--error: #ba1a1a;--on-error: #ffffff;--error-container: #ffdad6;--on-error-container: #410002;--background: #fffbff;--on-background: #1c1b1e;--surface: #fdf8fd;--on-surface: #1c1b1e;--surface-variant: #e7e0eb;--on-surface-variant: #49454e;--outline: #7a757f;--outline-variant: #cac4cf;--shadow: #000000;--scrim: #000000;--inverse-surface: #313033;--inverse-on-surface: #f4eff4;--inverse-primary: #cfbcff;--surface-dim: #ddd8dd;--surface-bright: #fdf8fd;--surface-container-lowest: #ffffff;--surface-container-low: #f7f2f7;--surface-container: #f2ecf1;--surface-container-high: #ece7eb;--surface-container-highest: #e6e1e6}body.dark{--primary: #cfbcff;--on-primary: #381e72;--primary-container: #4f378a;--on-primary-container: #e9ddff;--secondary: #cbc2db;--on-secondary: #332d41;--secondary-container: #4a4458;--on-secondary-container: #e8def8;--tertiary: #efb8c8;--on-tertiary: #4a2532;--tertiary-container: #633b48;--on-tertiary-container: #ffd9e3;--error: #ffb4ab;--on-error: #690005;--error-container: #93000a;--on-error-container: #ffb4ab;--background: #1c1b1e;--on-background: #e6e1e6;--surface: #141316;--on-surface: #e6e1e6;--surface-variant: #49454e;--on-surface-variant: #cac4cf;--outline: #948f99;--outline-variant: #49454e;--shadow: #000000;--scrim: #000000;--inverse-surface: #e6e1e6;--inverse-on-surface: #313033;--inverse-primary: #6750a4;--surface-dim: #141316;--surface-bright: #3a383c;--surface-container-lowest: #0f0e11;--surface-container-low: #1c1b1e;--surface-container: #201f22;--surface-container-high: #2b292d;--surface-container-highest: #363438}@font-face{font-family:Material Symbols Outlined;font-style:normal;font-weight:400;font-display:block;src:url(material-symbols-outlined.woff2) format("woff2"),url(https://cdn.jsdelivr.net/npm/beercss@3.12.11/dist/cdn/material-symbols-outlined.woff2) format("woff2")}@font-face{font-family:Material Symbols Rounded;font-style:normal;font-weight:400;font-display:block;src:url(material-symbols-rounded.woff2) format("woff2"),url(https://cdn.jsdelivr.net/npm/beercss@3.12.11/dist/cdn/material-symbols-rounded.woff2) format("woff2")}@font-face{font-family:Material Symbols Sharp;font-style:normal;font-weight:400;font-display:block;src:url(material-symbols-sharp.woff2) format("woff2"),url(https://cdn.jsdelivr.net/npm/beercss@3.12.11/dist/cdn/material-symbols-sharp.woff2) format("woff2")}@font-face{font-family:Material Symbols Subset;font-style:normal;font-weight:400;font-display:block;src:url(material-symbols-subset.woff2) format("woff2"),url(https://cdn.jsdelivr.net/npm/beercss@3.12.11/dist/cdn/material-symbols-subset.woff2) format("woff2")}*{-webkit-tap-highlight-color:transparent;position:relative;vertical-align:middle;color:inherit;margin:0;padding:0;border-radius:inherit;box-sizing:border-box}*:after,*:before{all:unset}body{color:var(--on-surface);background-color:var(--surface);overflow-x:hidden}label{font-size:.75rem;vertical-align:baseline}a,b,i,span,strong,em,code{vertical-align:baseline}a,button,.button{cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;border:none;font-family:inherit;outline:inherit;justify-content:center}a,button,.button,i,label{-webkit-user-select:none;user-select:none}body ::-webkit-scrollbar,body ::-webkit-scrollbar-thumb,body ::-webkit-scrollbar-button{background:none;inline-size:.4rem;block-size:.4rem}body :is(:hover,:focus)::-webkit-scrollbar-thumb{background:var(--outline);border-radius:1rem}:not(.grid,nav,.row)>*+:is(address,article,blockquote,code,.field,fieldset,form,.grid,h1,h2,h3,h4,h5,h6,nav,ol,p,pre,.row,section,aside,table,.tabs,ul):not([class*=margin],.right,.left,.top,.bottom){margin-block-start:1rem}:has(>[class*=margin]){padding:.1px}:is(a,button,.button,.chip):focus-visible{outline:.125rem solid var(--primary);outline-offset:.25rem}:is(nav,.row,li).group>:focus-visible{z-index:1}:is(button,.button,.chip)>:is(span,i,img,svg){pointer-events:none}.transparent{background-color:transparent!important;box-shadow:none!important;color:inherit!important}.primary{background-color:var(--primary)!important;color:var(--on-primary)!important}.primary-text{color:var(--primary)!important}.primary-border{border-color:var(--primary)!important}.primary-container{background-color:var(--primary-container)!important;color:var(--on-primary-container)!important}.secondary{background-color:var(--secondary)!important;color:var(--on-secondary)!important}.secondary-text{color:var(--secondary)!important}.secondary-border{border-color:var(--secondary)!important}.secondary-container{background-color:var(--secondary-container)!important;color:var(--on-secondary-container)!important}.tertiary{background-color:var(--tertiary)!important;color:var(--on-tertiary)!important}.tertiary-text{color:var(--tertiary)!important}.tertiary-border{border-color:var(--tertiary)!important}.tertiary-container{background-color:var(--tertiary-container)!important;color:var(--on-tertiary-container)!important}.error{background-color:var(--error)!important;color:var(--on-error)!important}.error-text{color:var(--error)!important}.error-border{border-color:var(--error)!important}.error-container{background-color:var(--error-container)!important;color:var(--on-error-container)!important}.background{background-color:var(--background)!important;color:var(--on-background)!important}.surface,.surface-dim,.surface-bright,.surface-container-lowest,.surface-container-low,.surface-container,.surface-container-high,.surface-container-highest{background-color:var(--surface)!important;color:var(--on-surface)!important}.surface-variant{background-color:var(--surface-variant)!important;color:var(--on-surface-variant)!important}.inverse-surface{background-color:var(--inverse-surface);color:var(--inverse-on-surface)}.inverse-primary{background-color:var(--inverse-primary);color:var(--primary)}.inverse-primary-text{color:var(--inverse-primary)!important}.inverse-primary-border{border-color:var(--inverse-primary)!important}.surface-dim{background-color:var(--surface-dim)!important}.surface-bright{background-color:var(--surface-bright)!important}.surface-container-lowest{background-color:var(--surface-container-lowest)!important}.surface-container{background-color:var(--surface-container)!important}.surface-container-high{background-color:var(--surface-container-high)!important}.surface-container-highest{background-color:var(--surface-container-highest)!important}.surface-container-low{background-color:var(--surface-container-low)!important}.black{background-color:#000!important}.black-border{border-color:#000!important}.black-text{color:#000!important}.white{background-color:#fff!important}.white-border{border-color:#fff!important}.white-text{color:#fff!important}.transparent-border{border-color:transparent!important}.transparent-text{color:transparent!important}.fill:not(i){background-color:var(--surface-variant)!important;color:var(--on-surface-variant)!important}.middle-align{display:flex;align-items:center!important}.bottom-align{display:flex;align-items:flex-end!important}.top-align{display:flex;align-items:flex-start!important}.left-align{text-align:start;justify-content:flex-start!important}.right-align{text-align:end;justify-content:flex-end!important}.center-align{text-align:center;justify-content:center!important}[class*=blur],[class*=blur].light{--_blur: 1rem;-webkit-backdrop-filter:blur(var(--_blur));backdrop-filter:blur(var(--_blur));color:var(--on-surface);background-color:#ffffff80!important}.dark [class*=blur],[class*=blur].dark{background-color:#00000080!important}.small-blur{--_blur: .5rem}.large-blur{--_blur: 1.5rem}.red,.red6{background-color:#f44336!important}.red-border{border-color:#f44336!important}.red-text{color:#f44336!important}.red1{background-color:#ffebee!important}.red2{background-color:#ffcdd2!important}.red3{background-color:#ef9a9a!important}.red4{background-color:#e57373!important}.red5{background-color:#ef5350!important}.red7{background-color:#e53935!important}.red8{background-color:#d32f2f!important}.red9{background-color:#c62828!important}.red10{background-color:#b71c1c!important}.pink,.pink6{background-color:#e91e63!important}.pink-border{border-color:#e91e63!important}.pink-text{color:#e91e63!important}.pink1{background-color:#fce4ec!important}.pink2{background-color:#f8bbd0!important}.pink3{background-color:#f48fb1!important}.pink4{background-color:#f06292!important}.pink5{background-color:#ec407a!important}.pink7{background-color:#d81b60!important}.pink8{background-color:#c2185b!important}.pink9{background-color:#ad1457!important}.pink10{background-color:#880e4f!important}.purple,.purple6{background-color:#9c27b0!important}.purple-border{border-color:#9c27b0!important}.purple-text{color:#9c27b0!important}.purple1{background-color:#f3e5f5!important}.purple2{background-color:#e1bee7!important}.purple3{background-color:#ce93d8!important}.purple4{background-color:#ba68c8!important}.purple5{background-color:#ab47bc!important}.purple7{background-color:#8e24aa!important}.purple8{background-color:#7b1fa2!important}.purple9{background-color:#6a1b9a!important}.purple10{background-color:#4a148c!important}.deep-purple,.deep-purple6{background-color:#673ab7!important}.deep-purple-border{border-color:#673ab7!important}.deep-purple-text{color:#673ab7!important}.deep-purple1{background-color:#ede7f6!important}.deep-purple2{background-color:#d1c4e9!important}.deep-purple3{background-color:#b39ddb!important}.deep-purple4{background-color:#9575cd!important}.deep-purple5{background-color:#7e57c2!important}.deep-purple7{background-color:#5e35b1!important}.deep-purple8{background-color:#512da8!important}.deep-purple9{background-color:#4527a0!important}.deep-purple10{background-color:#311b92!important}.indigo,.indigo6{background-color:#3f51b5!important}.indigo-border{border-color:#3f51b5!important}.indigo-text{color:#3f51b5!important}.indigo1{background-color:#e8eaf6!important}.indigo2{background-color:#c5cae9!important}.indigo3{background-color:#9fa8da!important}.indigo4{background-color:#7986cb!important}.indigo5{background-color:#5c6bc0!important}.indigo7{background-color:#3949ab!important}.indigo8{background-color:#303f9f!important}.indigo9{background-color:#283593!important}.indigo10{background-color:#1a237e!important}.blue,.blue6{background-color:#2196f3!important}.blue-border{border-color:#2196f3!important}.blue-text{color:#2196f3!important}.blue1{background-color:#e3f2fd!important}.blue2{background-color:#bbdefb!important}.blue3{background-color:#90caf9!important}.blue4{background-color:#64b5f6!important}.blue5{background-color:#42a5f5!important}.blue7{background-color:#1e88e5!important}.blue8{background-color:#1976d2!important}.blue9{background-color:#1565c0!important}.blue10{background-color:#0d47a1!important}.light-blue,.light-blue6{background-color:#03a9f4!important}.light-blue-border{border-color:#03a9f4!important}.light-blue-text{color:#03a9f4!important}.light-blue1{background-color:#e1f5fe!important}.light-blue2{background-color:#b3e5fc!important}.light-blue3{background-color:#81d4fa!important}.light-blue4{background-color:#4fc3f7!important}.light-blue5{background-color:#29b6f6!important}.light-blue7{background-color:#039be5!important}.light-blue8{background-color:#0288d1!important}.light-blue9{background-color:#0277bd!important}.light-blue10{background-color:#01579b!important}.cyan,.cyan6{background-color:#00bcd4!important}.cyan-border{border-color:#00bcd4!important}.cyan-text{color:#00bcd4!important}.cyan1{background-color:#e0f7fa!important}.cyan2{background-color:#b2ebf2!important}.cyan3{background-color:#80deea!important}.cyan4{background-color:#4dd0e1!important}.cyan5{background-color:#26c6da!important}.cyan7{background-color:#00acc1!important}.cyan8{background-color:#0097a7!important}.cyan9{background-color:#00838f!important}.cyan10{background-color:#006064!important}.teal,.teal6{background-color:#009688!important}.teal-border{border-color:#009688!important}.teal-text{color:#009688!important}.teal1{background-color:#e0f2f1!important}.teal2{background-color:#b2dfdb!important}.teal3{background-color:#80cbc4!important}.teal4{background-color:#4db6ac!important}.teal5{background-color:#26a69a!important}.teal7{background-color:#00897b!important}.teal8{background-color:#00796b!important}.teal9{background-color:#00695c!important}.teal10{background-color:#004d40!important}.green,.green6{background-color:#4caf50!important}.green-border{border-color:#4caf50!important}.green-text{color:#4caf50!important}.green1{background-color:#e8f5e9!important}.green2{background-color:#c8e6c9!important}.green3{background-color:#a5d6a7!important}.green4{background-color:#81c784!important}.green5{background-color:#66bb6a!important}.green7{background-color:#43a047!important}.green8{background-color:#388e3c!important}.green9{background-color:#2e7d32!important}.green10{background-color:#1b5e20!important}.light-green,.light-green6{background-color:#8bc34a!important}.light-green-border{border-color:#8bc34a!important}.light-green-text{color:#8bc34a!important}.light-green1{background-color:#f1f8e9!important}.light-green2{background-color:#dcedc8!important}.light-green3{background-color:#c5e1a5!important}.light-green4{background-color:#aed581!important}.light-green5{background-color:#9ccc65!important}.light-green7{background-color:#7cb342!important}.light-green8{background-color:#689f38!important}.light-green9{background-color:#558b2f!important}.light-green10{background-color:#33691e!important}.lime,.lime6{background-color:#cddc39!important}.lime-border{border-color:#cddc39!important}.lime-text{color:#cddc39!important}.lime1{background-color:#f9fbe7!important}.lime2{background-color:#f0f4c3!important}.lime3{background-color:#e6ee9c!important}.lime4{background-color:#dce775!important}.lime5{background-color:#d4e157!important}.lime7{background-color:#c0ca33!important}.lime8{background-color:#afb42b!important}.lime9{background-color:#9e9d24!important}.lime10{background-color:#827717!important}.yellow,.yellow6{background-color:#ffeb3b!important}.yellow-border{border-color:#ffeb3b!important}.yellow-text{color:#ffeb3b!important}.yellow1{background-color:#fffde7!important}.yellow2{background-color:#fff9c4!important}.yellow3{background-color:#fff59d!important}.yellow4{background-color:#fff176!important}.yellow5{background-color:#ffee58!important}.yellow7{background-color:#fdd835!important}.yellow8{background-color:#fbc02d!important}.yellow9{background-color:#f9a825!important}.yellow10{background-color:#f57f17!important}.amber,.amber6{background-color:#ffc107!important}.amber-border{border-color:#ffc107!important}.amber-text{color:#ffc107!important}.amber1{background-color:#fff8e1!important}.amber2{background-color:#ffecb3!important}.amber3{background-color:#ffe082!important}.amber4{background-color:#ffd54f!important}.amber5{background-color:#ffca28!important}.amber7{background-color:#ffb300!important}.amber8{background-color:#ffa000!important}.amber9{background-color:#ff8f00!important}.amber10{background-color:#ff6f00!important}.orange,.orange6{background-color:#ff9800!important}.orange-border{border-color:#ff9800!important}.orange-text{color:#ff9800!important}.orange1{background-color:#fff3e0!important}.orange2{background-color:#ffe0b2!important}.orange3{background-color:#ffcc80!important}.orange4{background-color:#ffb74d!important}.orange5{background-color:#ffa726!important}.orange7{background-color:#fb8c00!important}.orange8{background-color:#f57c00!important}.orange9{background-color:#ef6c00!important}.orange10{background-color:#e65100!important}.deep-orange,.deep-orange6{background-color:#ff5722!important}.deep-orange-border{border-color:#ff5722!important}.deep-orange-text{color:#ff5722!important}.deep-orange1{background-color:#fbe9e7!important}.deep-orange2{background-color:#ffccbc!important}.deep-orange3{background-color:#ffab91!important}.deep-orange4{background-color:#ff8a65!important}.deep-orange5{background-color:#ff7043!important}.deep-orange7{background-color:#f4511e!important}.deep-orange8{background-color:#e64a19!important}.deep-orange9{background-color:#d84315!important}.deep-orange10{background-color:#bf360c!important}.brown,.brown6{background-color:#795548!important}.brown-border{border-color:#795548!important}.brown-text{color:#795548!important}.brown1{background-color:#efebe9!important}.brown2{background-color:#d7ccc8!important}.brown3{background-color:#bcaaa4!important}.brown4{background-color:#a1887f!important}.brown5{background-color:#8d6e63!important}.brown7{background-color:#6d4c41!important}.brown8{background-color:#5d4037!important}.brown9{background-color:#4e342e!important}.brown10{background-color:#3e2723!important}.blue-grey,.blue-grey6{background-color:#607d8b!important}.blue-grey-border{border-color:#607d8b!important}.blue-grey-text{color:#607d8b!important}.blue-grey1{background-color:#eceff1!important}.blue-grey2{background-color:#cfd8dc!important}.blue-grey3{background-color:#b0bec5!important}.blue-grey4{background-color:#90a4ae!important}.blue-grey5{background-color:#78909c!important}.blue-grey7{background-color:#546e7a!important}.blue-grey8{background-color:#455a64!important}.blue-grey9{background-color:#37474f!important}.blue-grey10{background-color:#263238!important}.grey,.grey6{background-color:#9e9e9e!important}.grey-border{border-color:#9e9e9e!important}.grey-text{color:#9e9e9e!important}.grey1{background-color:#fafafa!important}.grey2{background-color:#f5f5f5!important}.grey3{background-color:#eee!important}.grey4{background-color:#e0e0e0!important}.grey5{background-color:#bdbdbd!important}.grey7{background-color:#757575!important}.grey8{background-color:#616161!important}.grey9{background-color:#424242!important}.grey10{background-color:#212121!important}.horizontal{display:inline-flex;flex-direction:row!important;gap:1rem;inline-size:auto!important;max-inline-size:none!important}.horizontal>*{margin-block:0!important}.vertical{display:flex;flex-direction:column!important}:is(a,button,.button,.chip).vertical{display:inline-flex;gap:.25rem;block-size:auto!important;max-block-size:none!important;padding-block:.5rem}.vertical>*{margin-inline:0!important}.no-elevate{box-shadow:none!important}.small-elevate,.elevate{box-shadow:var(--elevate1)!important}.medium-elevate{box-shadow:var(--elevate2)!important}.large-elevate{box-shadow:var(--elevate3)!important}.round,[class*=-round]{--_round: 2rem;border-radius:var(--_round)!important}.small-round{--_round: .5rem}.large-round{--_round: 3.5rem}.no-round,.square,.top-round,.bottom-round,.left-round,.right-round{border-radius:.5rem!important}.top-round{border-start-start-radius:var(--_round)!important;border-start-end-radius:var(--_round)!important}.bottom-round{border-end-end-radius:var(--_round)!important;border-end-start-radius:var(--_round)!important}.left-round{border-start-start-radius:var(--_round)!important;border-end-start-radius:var(--_round)!important}.right-round{border-start-end-radius:var(--_round)!important;border-end-end-radius:var(--_round)!important}.circle:not(.extend){border-radius:50%}:is(.circle,.square):is(button,.button,.chip){padding:0;block-size:var(--_size);inline-size:var(--_size)}:is(.circle,.square)>span{display:none}:is(.circle,.square).round{border-radius:1rem!important}.border:not(table,.field,.list,menu,article){box-sizing:border-box;border:.0625rem solid var(--outline);background-color:transparent;box-shadow:none}.no-border{border-color:transparent!important}.border:not(.extend,.circle,.square,.badge){box-sizing:content-box}.margin,[class*=-margin]:not(.left-margin,.right-margin,.top-margin,.bottom-margin,.horizontal-margin,.vertical-margin){margin:var(--_margin)!important}.margin,[class*=-margin]{--_margin: 1rem}.no-margin{--_margin: 0}.auto-margin{--_margin: auto}.tiny-margin{--_margin: .25rem}.small-margin{--_margin: .5rem}.large-margin{--_margin: 1.5rem}.left-margin,.horizontal-margin{margin-inline-start:var(--_margin)!important}.right-margin,.horizontal-margin{margin-inline-end:var(--_margin)!important}.top-margin,.vertical-margin{margin-block-start:var(--_margin)!important}.bottom-margin,.vertical-margin{margin-block-end:var(--_margin)!important}.no-opacity{opacity:1!important}.opacity{opacity:0!important}.small-opacity{opacity:.75!important}.medium-opacity{opacity:.5!important}.large-opacity{opacity:.25!important}.padding,[class*=-padding]:not(.left-padding,.right-padding,.top-padding,.bottom-padding,.horizontal-padding,.vertical-padding){padding:var(--_padding)!important}.padding,[class*=-padding]{--_padding: 1rem}.no-padding{--_padding: 0 !important}.tiny-padding{--_padding: .25rem !important}.small-padding{--_padding: .5rem !important}.large-padding{--_padding: 1.5rem !important}.left-padding,.horizontal-padding{padding-inline-start:var(--_padding)!important}.right-padding,.horizontal-padding{padding-inline-end:var(--_padding)!important}.top-padding,.vertical-padding{padding-block-start:var(--_padding)!important}.bottom-padding,.vertical-padding{padding-block-end:var(--_padding)!important}.front{z-index:10!important}.back{z-index:-10!important}.left{inset-inline-start:0}.right{inset-inline-end:0}.top{inset-block-start:0}.bottom{inset-block-end:0}.center{inset-inline-start:50%;transform:translate(-50%)}[dir=rtl] .center{transform:translate(50%)}.middle{inset-block-start:50%;transform:translateY(-50%)}.middle.center{transform:translate(-50%,-50%)}[dir=rtl] .middle.center{transform:translate(50%,-50%)}.ripple{--_duration: .6s}.fast-ripple{--_duration: .2s}.slow-ripple{--_duration: 1.8s}.ripple-js{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;overflow:hidden}.ripple-js>div{position:absolute;border-radius:50%;background:currentColor;opacity:.3;transform:scale(0);animation:to-ripple var(--_duration) linear}@keyframes to-ripple{to{transform:scale(4);opacity:0}}.scroll{overflow:auto;min-inline-size:0}.no-scroll{overflow:hidden}.shadow{background-color:#00000050}:is(.left-shadow,.right-shadow,.top-shadow,.bottom-shadow){background-color:transparent!important}.left-shadow{background-image:linear-gradient(to right,black,transparent)!important}.right-shadow{background-image:linear-gradient(to left,black,transparent)!important}.bottom-shadow{background-image:linear-gradient(to top,black,transparent)!important}.top-shadow{background-image:linear-gradient(to bottom,black,transparent)!important}[class*=-width]{max-inline-size:100%}.auto-width{inline-size:auto}.small-width{inline-size:12rem!important}.medium-width{inline-size:24rem!important}.large-width{inline-size:36rem!important}.auto-height{block-size:auto}.small-height{block-size:12rem!important}.medium-height{block-size:24rem!important}.large-height{block-size:36rem!important}.wrap{display:block;white-space:normal}.no-wrap:not(menu){display:flex;white-space:nowrap}.tiny-space:not(nav,.row,.grid,table,.tooltip,.list,menu,.shape){block-size:.5rem}:is(.space,.small-space):not(nav,.row,.grid,table,.tooltip,.list,menu,.shape){block-size:1rem}.medium-space:not(nav,.row,.grid,table,.tooltip,.list,menu,.shape){block-size:2rem}.large-space:not(nav,.row,.grid,table,.tooltip,.list,menu,.shape){block-size:3rem}.extra-space:not(nav,.row,.grid,table,.tooltip,.list,menu,.shape){block-size:4rem}.responsive{inline-size:-webkit-fill-available;inline-size:-moz-available}@media only screen and (max-width: 600px){:is(.m,.l):not(.s){display:none!important}}@media only screen and (min-width: 601px) and (max-width: 992px){:is(.s,.l):not(.m){display:none!important}}@media only screen and (min-width: 993px){:is(.m,.s):not(.l){display:none!important}}html{font-size:var(--size)}body{font-family:var(--font);font-size:.875rem;line-height:1.5rem;letter-spacing:.0313rem}h1,h2,h3,h4,h5,h6{font-weight:400;display:block;align-items:center;line-height:normal}h1{font-size:3.5625rem}h2{font-size:2.8125rem}h3{font-size:2.25rem}h4{font-size:2rem}h5{font-size:1.75rem}h6{font-size:1.5rem}h1.small{font-size:3.0625rem}h2.small{font-size:2.3125rem}h3.small{font-size:1.75rem}h4.small{font-size:1.5rem}h5.small{font-size:1.25rem}h6.small{font-size:1rem}h1.large{font-size:4.0625rem}h2.large{font-size:3.3125rem}h3.large{font-size:2.75rem}h4.large{font-size:2.5rem}h5.large{font-size:2.25rem}h6.large{font-size:2rem}.link{color:var(--primary)!important}.inverse-link{color:var(--inverse-primary)!important}.truncate{overflow:hidden;white-space:nowrap!important;text-overflow:ellipsis;flex:inherit}.truncate>*{white-space:nowrap!important}.small-text{font-size:.75rem}.medium-text{font-size:.875rem}.large-text{font-size:1rem}.upper{text-transform:uppercase}.lower{text-transform:lowercase}.capitalize{text-transform:capitalize}.bold{font-weight:700}.overline{text-decoration:line-through}.underline{text-decoration:underline}.italic{font-style:italic}p{margin:.5rem 0}.no-line{line-height:normal}.tiny-line{line-height:1.25rem}.small-line{line-height:1.5rem}.medium-line{line-height:1.75rem}.large-line{line-height:2rem}.extra-line{line-height:2.25rem}pre{border-radius:0;background-color:var(--surface-container);white-space:pre-wrap;padding:1rem;border-inline-start:.25rem solid var(--primary);font-family:inherit}blockquote{border-radius:0;padding:1rem;border-inline-start:.25rem solid var(--primary);font-family:inherit}code{border-radius:0;background-color:var(--surface-container);white-space:pre-wrap;padding:.25rem}pre>code,blockquote>code{padding:0}.scroll>code{white-space:pre}pre:has(>code){direction:ltr;text-align:start}sub{vertical-align:sub}sup{vertical-align:super}:is(.wave,.chip,.button,button,nav.tabbed>a,.tabs>a,nav.toolbar>a):not(.slow-ripple,.ripple,.fast-ripple):after,nav:is(.left,.right,.bottom,.top).max>a:after,nav:is(.left,.right,.bottom,.top).max>:is(ol,ul)>li>a:after,nav:is(.left,.right,.bottom,.top):not(.max)>a>i:after,nav:is(.left,.right,.bottom,.top):not(.max)>:is(ol,ul)>li>a>i:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;border-radius:inherit;inline-size:100%;block-size:100%;background-position:center;background-image:radial-gradient(circle,currentColor 1%,transparent 1%);opacity:0;transition:none;pointer-events:none}:is(.wave,.chip,.button,button,nav.tabbed>a,.tabs>a,nav.toolbar>a):not(.slow-ripple,.ripple,.fast-ripple):is(:focus-visible,:hover):after,nav:is(.left,.right,.bottom,.top).max>a:not(.button,.chip):is(:focus-visible,:hover):after,nav:is(.left,.right,.bottom,.top).max>:is(ol,ul)>li>a:not(.button,.chip):is(:focus-visible,:hover):after,nav:is(.left,.right,.bottom,.top):not(.max)>a:not(.button,.chip):is(:focus-visible,:hover)>i:after,nav:is(.left,.right,.bottom,.top):not(.max)>:is(ol,ul)>li>a:not(.button,.chip):is(:focus-visible,:hover)>i:after{background-size:22500%;opacity:.1;transition:background-size var(--speed2) linear}:is(.wave,.chip,.button,button,nav.tabbed>a,.tabs>a,nav.toolbar>a,nav.max>a):not(.slow-ripple,.ripple,.fast-ripple):active:after,nav:is(.left,.right,.bottom,.top).max>a:active:after,nav:is(.left,.right,.bottom,.top).max>:is(ol,ul)>li>a:active:after,nav:is(.left,.right,.bottom,.top):not(.max)>a:active>i:after,nav:is(.left,.right,.bottom,.top):not(.max)>:is(ol,ul)>li>a:active>i:after{background-size:0%;opacity:0;transition:none}.no-wave:after,.no-wave:is(:hover,:active):after{display:none}.zoom,.tiny-zoom{zoom:2}.small-zoom{zoom:3}.medium-zoom{zoom:4}.large-zoom{zoom:5}.extra-zoom{zoom:6}.badge{--_x: 0;--_y: -100%;display:inline-flex;align-items:center;justify-content:center;position:absolute;font-size:.6875rem;text-transform:none;z-index:2;padding:0 .25rem;min-block-size:1rem;min-inline-size:1rem;background-color:var(--error);color:var(--on-error);line-height:normal;border-radius:1rem;inset:50% auto auto 50%;transform:translate(var(--_x, 50%),var(--_y, -50%));font-family:var(--font)}.badge.top{--_y: -100%}.badge.bottom{--_y: 0}.badge.left{--_x: -100%}.badge.right{--_x: 0}.badge.border{border-color:var(--error);color:var(--error);background-color:var(--surface)}.badge:is(.circle,.square){text-align:center;inline-size:auto;block-size:auto;padding:0 .25rem;border-radius:1rem}.badge.square{border-radius:0}.badge.min>*{display:none}.badge.min{clip-path:circle(18.75% at 50% 50%)}nav:is(.left,.right,.top,.bottom)>a>.badge,nav:is(.left,.right,.top,.bottom)>:is(ol,ul)>li>a>.badge{inset:1rem auto auto 50%}.badge.none{inset:auto!important;transform:none;position:relative;margin:0 .125rem}header,footer{display:grid;align-content:center;border-radius:0;padding:0 1rem}:is(dialog,article)>:is(header,footer){padding-inline:0;top:0;right:0;bottom:0;left:0}header{min-block-size:4rem}footer{min-block-size:5rem}:is(header,footer,menu>*).fixed{position:sticky;top:0;right:0;bottom:0;left:0;z-index:11;background-color:inherit}header.fixed{inset:calc(-1 * var(--_padding)) 0 0 0;margin-block-start:calc(-1 * var(--_padding))}footer.fixed{inset:0 0 calc(-1 * var(--_padding)) 0;margin-block-end:calc(-1 * var(--_padding))}:is(header,footer).fixed.min{margin-inline:auto}dialog>:is(header,footer){background:none}dialog>:is(header,footer).fixed{background-color:inherit}:is(main,header,footer,section).responsive{max-inline-size:min(100vw,75rem);margin:0 auto}:is(main,header,footer,section).responsive.max{max-inline-size:none}:has(>main)>:is(header,footer).fixed{transform:none;box-sizing:content-box;position:sticky;top:0;right:0;bottom:0;left:0;z-index:12}:has(>main)>header{padding-block-start:var(--top)}:has(>main)>footer{padding-block-end:var(--bottom)}nav.top~header,nav.bottom~footer{padding-block:0}nav.top~header.fixed{inset-block:calc(var(--top) + 4.5rem) 0}nav.bottom~footer.fixed{inset-block:0 calc(var(--bottom) + 4.5rem)}:is(nav,.row)>header{background-color:inherit}.button,button{--_padding: 1rem;--_size: 2.5rem;box-sizing:content-box;display:inline-flex;align-items:center;justify-content:center;block-size:var(--_size);font-size:.875rem;font-weight:500;color:var(--on-primary);padding:0 var(--_padding);background-color:var(--primary);margin:0 .5rem;border-radius:var(--_size);transition:transform var(--speed3),border-radius var(--speed2),padding var(--speed3);-webkit-user-select:none;user-select:none;gap:.5rem;line-height:normal}:is(button,.button).small{--_size: 2rem;--_padding: .75rem}:is(button,.button).large{--_size: 3rem;--_padding: 1.25rem}:is(.button,button):is(.extra,.extend){--_size: 3.5rem;font-size:1rem;--_padding: 1.5rem}:is(button,.button):is(.square,.circle){--_padding: 0}:is(button,.button).border{border-color:var(--outline-variant);color:var(--primary)}.extend>span{display:none}.extend:is(:hover,.active){inline-size:auto;--_padding: 1.5rem;padding:0 var(--_padding)}.extend:is(:hover,.active)>i+span{display:inherit;margin-inline-start:var(--_padding)}.extend:is(:hover,.active)>:is(img,svg)+span{display:inherit;margin-inline-start:calc(1rem + var(--_padding))}:is(.button,button)[disabled]{opacity:.5;cursor:not-allowed}.button[disabled]{pointer-events:none}:is(.button,button)[disabled]:before,:is(.button,button)[disabled]:after{display:none}:is(.button,button):not(.chip,.extend).fill{background-color:var(--secondary-container)!important;color:var(--on-secondary-container)!important}:is(.button,button):not(.chip,.extend).active{background-color:var(--primary-container);color:var(--on-primary-container)}:is(.button,button):not(.chip,.extend).fill.active{background-color:var(--secondary)!important;color:var(--on-secondary)!important}:is(.button,button):not(.chip,.extend).border.active{background-color:var(--inverse-surface)!important;color:var(--inverse-on-surface)!important;border-color:var(--inverse-surface)!important}:is(.button,button):not(.chip):active,:is(.button,button):not(.chip).active{border-radius:.5rem!important}article{--_padding: 1rem;box-shadow:var(--elevate1);background-color:var(--surface-container-low);color:var(--on-surface);padding:var(--_padding);border-radius:.75rem;display:block;transition:transform var(--speed3),border-radius var(--speed3),padding var(--speed3)}article.small{block-size:12rem}article.medium{block-size:20rem}article.large{block-size:32rem}article.border{box-shadow:none;border:.0625rem solid var(--outline-variant)}.chip{--_padding: .75rem;--_size: 2rem;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;block-size:var(--_size);min-inline-size:var(--_size);font-size:.875rem;font-weight:500;background-color:transparent;border:.0625rem solid var(--outline-variant);color:var(--on-surface-variant);padding:0 var(--_padding);margin:0 .5rem;text-transform:none;border-radius:.5rem;transition:transform var(--speed3),border-radius var(--speed3),padding var(--speed3);-webkit-user-select:none;user-select:none;gap:.5rem;line-height:normal;letter-spacing:normal}.chip.medium{--_size: 2.5rem;--_padding: 1rem}.chip.large{--_padding: 1.25rem;--_size: 3rem}.chip.fill{border:none}dialog{--_padding: 1.5rem;--_top: calc(var(--_padding) + var(--top));--_bottom: calc(var(--_padding) + var(--bottom));display:block;visibility:hidden;border:none;opacity:0;position:fixed;box-shadow:var(--elevate2);color:var(--on-surface);background-color:var(--surface-container-high);padding:var(--_padding);z-index:100;inset:10% auto auto 50%;min-inline-size:20rem;max-inline-size:100%;max-block-size:80%;overflow-x:hidden;overflow-y:auto;transition:all var(--speed3),0s background-color;border-radius:1.75rem;transform:translate(-50%,-4rem);outline:none}dialog.small{inline-size:25%;block-size:25%}dialog.medium{inline-size:50%;block-size:50%}dialog.large{inline-size:75%;block-size:75%}dialog:is(.active,[open]){visibility:visible;opacity:1;transform:translate(-50%)}dialog:popover-open{visibility:visible;opacity:1;transform:translate(-50%)}dialog:is(.top,.right,.bottom,.left,.max){--_padding: 1rem}dialog:is(.top,.bottom){opacity:1;block-size:auto;inline-size:100%;min-inline-size:auto;max-block-size:100%}dialog.top{inset:0 auto auto 0;transform:translateY(-100%);border-radius:0 0 1rem 1rem;padding-block-start:var(--_top)}dialog.bottom{inset:auto auto 0 0;transform:translateY(100%);border-radius:1rem 1rem 0 0;padding-block-end:var(--_bottom)}dialog:is(.left,.right){opacity:1;inset:0 auto auto 0;inline-size:auto;block-size:100%;max-block-size:100%;background-color:var(--surface);padding-block:var(--_top) var(--_bottom)}[dir=rtl] dialog.right,dialog.left{inset:0 auto auto 0;border-radius:0 1rem 1rem 0;transform:translate(-100%)}[dir=rtl] dialog.left,dialog.right{inset:0 0 auto auto;border-radius:1rem 0 0 1rem;transform:translate(100%)}dialog.max{inset:0 auto auto 0;inline-size:100%;block-size:100%;max-inline-size:100%;max-block-size:100%;transform:translateY(4rem);background-color:var(--surface);border-radius:0;padding-block:var(--_top) var(--_bottom)}dialog:is(.active,[open]):is(.left,.right,.top,.bottom,.max){transform:translate(0)}dialog:popover-open:is(.left,.right,.top,.bottom,.max){transform:translate(0)}dialog.small:is(.left,.right){inline-size:20rem}dialog.medium:is(.left,.right){inline-size:32rem}dialog.large:is(.left,.right){inline-size:44rem}dialog.small:is(.top,.bottom){block-size:16rem}dialog.medium:is(.top,.bottom){block-size:24rem}dialog.large:is(.top,.bottom){block-size:32rem}@media (pointer: coarse){body:has(dialog[open],dialog.active){overflow:hidden}}hr,[class*=divider]{all:unset;inline-size:-webkit-fill-available;min-block-size:auto;block-size:.0625rem;background-color:var(--outline-variant);display:block}hr+*,[class*=divider]+*{margin:0!important}hr.medium,.medium-divider{margin:1rem 0!important}hr.large,.large-divider{margin:1.5rem 0!important}hr.small,.small-divider{margin:.5rem 0!important}li:has(>hr),li:has(>.divider){padding:0!important;align-self:normal!important;min-inline-size:auto!important;min-block-size:auto!important;inline-size:-webkit-fill-available}hr.vertical,.divider.vertical,li:has(>hr.vertical),li:has(>.divider.vertical){padding:0!important;align-self:center!important;min-inline-size:auto;min-block-size:1.5rem;inline-size:.0625rem}summary,summary:focus{list-style-type:none;cursor:pointer;outline:none}summary::-webkit-details-marker{display:none}.field{--_size: 3rem;--_start: 1.2rem;block-size:var(--_size);margin-block-end:2rem;border-radius:.25rem .25rem 0 0;max-block-size:12rem}.grid>*>.field{margin-block-end:1rem}.grid>*>.field+.field{margin-block-start:2rem}.grid.no-space>*>.field+.field{margin-block-start:1rem}.grid.medium-space>*>.field+.field{margin-block-start:2.5rem}.grid.large-space>*>.field+.field{margin-block-start:3rem}.field.small{--_size: 2.5rem;--_start: 1rem}.field.large{--_size: 3.5rem;--_start: 1.4rem}.field.extra{--_size: 4rem;--_start: 1.6rem}.field.border{border-radius:.25rem}.field.round.small{border-radius:1.25rem}.field.round{border-radius:1.5rem}.field.round.large{border-radius:1.75rem}.field.round.extra{border-radius:2rem}.field>:is(i,img,svg,progress,a:not(.helper,.error)){position:absolute;inset:50% auto auto auto;transform:translateY(-50%);cursor:pointer;z-index:0;inline-size:1.5rem;block-size:1.5rem}.field>:is(i,img,svg,progress,a:not(.helper,.error)),[dir=rtl] .field>:is(i,img,svg,progress,a:not(.helper,.error)):first-child{inset:50% 1rem auto auto}.field>:is(i,img,svg,progress,a:not(.helper,.error)):first-child,[dir=rtl] .field>:is(i,img,svg,progress,a:not(.helper,.error)){inset:50% auto auto 1rem}.field.invalid>i{color:var(--error)}.field>progress.circle{inset-block-start:calc(50% - .75rem)!important;border-width:.1875rem}.field>a:not(.helper,.error){z-index:10}.field>a>:is(i,img,svg,progress,a:not(.helper,.error)){inline-size:1.5rem;block-size:1.5rem}.field>:is(input,textarea,select){all:unset;position:relative;display:flex;align-items:center;box-sizing:border-box;border-radius:inherit;border:.0625rem solid transparent;padding:0 .9375rem;font-family:inherit;font-size:1rem;inline-size:100%;block-size:100%;outline:none;z-index:1;background:none;resize:none;text-align:start;cursor:text}input::-webkit-date-and-time-value{text-align:start}:is(input,select,textarea):is(:-webkit-autofill,:autofill){-webkit-background-clip:text;-webkit-text-fill-color:var(--on-surface)}.field>:is(input,textarea,select):focus{border:.125rem solid transparent;padding:0 .875rem}.field.min>textarea{overflow:hidden auto;position:absolute;top:0;right:0;bottom:0;left:0}input[type=file],input[type=color],:not(.field)>input[type^=date],:not(.field)>input[type^=time],input::-webkit-calendar-picker-indicator{opacity:0;position:absolute;top:0;right:0;bottom:0;left:0;inline-size:100%;block-size:100%;margin:0;padding:0;border:0;outline:0;z-index:2!important}input::-webkit-search-decoration,input::-webkit-search-cancel-button,input::-webkit-search-results-button,input::-webkit-search-results-decoration,input::-webkit-inner-spin-button,input::-webkit-outer-spin-button{display:none}input[type=number]{-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.field.border>:is(input,textarea,select){border-color:var(--outline)}.field.border>:is(input,textarea,select):focus{border-color:var(--primary)}.field.round>:is(input,textarea,select){padding-inline:1.4376rem}.field.round>:is(input,textarea,select):focus{padding-inline:1.375rem}.field.prefix>:is(input,textarea,select){padding-inline-start:2.9375rem}.field.prefix>.slider{margin-inline-start:3.5rem}.field.prefix>:is(input,textarea,select):focus{padding-inline-start:2.875rem}.field.suffix>:is(input,textarea,select){padding-inline-end:2.9375rem}.field.suffix>.slider{margin-inline-end:3.5rem}.field.suffix>:is(input,textarea,select):focus{padding-inline-end:2.875rem}.field:not(.border,.round)>:is(input,textarea,select){border-block-end-color:var(--outline)}.field:not(.border,.round)>:is(input,textarea,select):focus{border-block-end-color:var(--primary)}.field.round:not(.border,.fill)>:is(input,textarea,select),.field.round:not(.border)>:is(input,textarea,select):focus{box-shadow:var(--elevate1)}.field.round:not(.border,.fill)>:is(input,textarea,select):focus{box-shadow:var(--elevate2)}.field.invalid:not(.border,.round)>:is(input,textarea,select),.field.invalid:not(.border,.round)>:is(input,textarea,select):focus{border-block-end-color:var(--error)}.field.invalid.border>:is(input,textarea,select),.field.invalid.border>:is(input,textarea,select):focus{border-color:var(--error)}.field:has(>:disabled){opacity:.5;cursor:not-allowed}.field>:disabled{cursor:not-allowed}.field.textarea.small:not(.min){--_size: 5rem}.field.textarea:not(.min){--_size: 5.5rem}.field.textarea.large:not(.min){--_size: 6rem}.field.textarea.extra:not(.min){--_size: 6.5rem}.field>select{-webkit-user-select:none;user-select:none}@-moz-document url-prefix(){.field>select:focus{background-color:var(--surface)}.field.fill>select:focus{background-color:var(--surface-variant)}.field>select:focus+label{z-index:1}}.field>select>option{background-color:var(--surface)}.field.label>:is(input,select){padding-block-start:1rem}.field.label.border:not(.fill)>:is(input,select){padding-block-start:0}.field>textarea{padding-block-start:var(--_start);white-space:pre-wrap;overflow-wrap:break-word}.field>textarea:focus{padding-block-start:calc(var(--_start) - .06rem)}.field:not(.label)>textarea,.field.border.label:not(.fill)>textarea{padding-block-start:calc(var(--_start) - .5rem)}.field:not(.label)>textarea:focus,.field.border.label:not(.fill)>textarea:focus{padding-block-start:calc(var(--_start) - .56rem)}.field.label>label{--_start: 1rem;position:absolute;inset:-.5rem 1rem 0 var(--_start);display:flex;block-size:calc(var(--_size) + 1rem);line-height:calc(var(--_size) + 1rem);font-size:1rem;transition:all .2s;gap:.25rem;white-space:nowrap}.field.label.round>label{inset:-.5rem 1.75rem 0 var(--_start)}.field.label.textarea:not(.min)>label{block-size:calc(var(--_size) - 1.5rem);line-height:calc(var(--_size) - 1.5rem)}.field.label.border.prefix:not(.fill)>:is(label.active,:focus+label,[placeholder]:not(:placeholder-shown)+label,select+label){--_start: 1rem}.field.label.round>label,.field.label.border.prefix.round:not(.fill)>:is(label.active,:focus+label,[placeholder]:not(:placeholder-shown)+label,select+label){--_start: 1.5rem}.field.label.prefix>label{--_start: 3rem}.field.label>:is(label.active,:focus+label,[placeholder]:not(:placeholder-shown)+label,select+label){block-size:2.5rem;line-height:2.5rem;font-size:.75rem}.field.label.border:not(.fill)>:is(label.active,:focus+label,[placeholder]:not(:placeholder-shown)+label,select+label){block-size:1rem;line-height:1rem}.field.label.border:not(.fill)>label:after{content:"";display:block;margin:.5rem 0 0;border-block-start:.0625rem solid var(--outline);block-size:1rem;transition:none;flex:auto}.field.label.border:not(.fill)>:focus+label:after{border-block-start:.125rem solid var(--primary)}.field.label.border:not(.fill)>:is(input,textarea):is(:focus,[placeholder]:not(:placeholder-shown),.active),.field.label.border:not(.fill)>select{clip-path:polygon(-2% -2%,.75rem -2%,.75rem .5rem,calc(100% - 1rem) .5rem,calc(100% - 1rem) -2%,102% -2%,102% 102%,-2% 102%)}[dir=rtl] .field.label.border:not(.fill)>:is(input,textarea):is(:focus,[placeholder]:not(:placeholder-shown),.active),[dir=rtl] .field.label.border:not(.fill)>select{clip-path:polygon(-2% -2%,1rem -2%,1rem .5rem,calc(100% - .75rem) .5rem,calc(100% - .75rem) -2%,102% -2%,102% 102%,-2% 102%)}.field.label.border.round:not(.fill)>:is(input,textarea):is(:focus,[placeholder]:not(:placeholder-shown),.active),.field.label.border.round:not(.fill)>select{clip-path:polygon(-2% -2%,1.25rem -2%,1.25rem .5rem,calc(100% - 1.75rem) .5rem,calc(100% - 1.75rem) -2%,102% -2%,102% 102%,-2% 102%)}[dir=rtl] .field.label.border.round:not(.fill)>:is(input,textarea):is(:focus,[placeholder]:not(:placeholder-shown),.active),[dir=rtl] .field.label.border.round:not(.fill)>select{clip-path:polygon(-2% -2%,1.75rem -2%,1.75rem .5rem,calc(100% - 1.25rem) .5rem,calc(100% - 1.25rem) -2%,102% -2%,102% 102%,-2% 102%)}.field.label>:focus+label{color:var(--primary)}.field.label.invalid>label,.field.label.invalid>label:after{color:var(--error)!important;border-color:var(--error)!important}.field.label>label>a{block-size:inherit;line-height:inherit;inline-size:1rem}.field.label>label>a>:is(i,img,svg){block-size:1rem;line-height:1rem;inline-size:1rem;font-size:1rem}.field>:is(.helper,.error){position:absolute;inset:auto auto 0 1rem;transform:translateY(100%);font-size:.75rem;background:none!important;padding-block-start:.125rem}[dir=rtl] .field>:is(.helper,.error){inset:auto 1rem 0 auto}a.helper{color:var(--primary)}.field>.error{color:var(--error)!important}.field.round>:is(.helper,.error){inset-inline-start:1.5rem}.field.invalid>.helper{display:none}table td>.field{margin:0}fieldset{border-radius:.25rem;padding:1rem;border:.0625rem solid var(--outline-variant)}fieldset>legend{margin:0 -.25rem;padding:0 .25rem}fieldset>legend+*{margin-block-start:0!important}.grid{--_gap: 1rem;display:grid;grid-template-columns:repeat(12,1fr);gap:var(--_gap);block-size:auto}.grid.no-space{--_gap: 0rem}.grid.medium-space{--_gap: 1.5rem}.grid.large-space{--_gap: 2rem}.grid>*{margin:0}.s1{grid-area:auto/span 1}.s2{grid-area:auto/span 2}.s3{grid-area:auto/span 3}.s4{grid-area:auto/span 4}.s5{grid-area:auto/span 5}.s6{grid-area:auto/span 6}.s7{grid-area:auto/span 7}.s8{grid-area:auto/span 8}.s9{grid-area:auto/span 9}.s10{grid-area:auto/span 10}.s11{grid-area:auto/span 11}.s12{grid-area:auto/span 12}@media only screen and (min-width: 601px){.m1{grid-area:auto/span 1}.m2{grid-area:auto/span 2}.m3{grid-area:auto/span 3}.m4{grid-area:auto/span 4}.m5{grid-area:auto/span 5}.m6{grid-area:auto/span 6}.m7{grid-area:auto/span 7}.m8{grid-area:auto/span 8}.m9{grid-area:auto/span 9}.m10{grid-area:auto/span 10}.m11{grid-area:auto/span 11}.m12{grid-area:auto/span 12}}@media only screen and (min-width: 993px){.l1{grid-area:auto/span 1}.l2{grid-area:auto/span 2}.l3{grid-area:auto/span 3}.l4{grid-area:auto/span 4}.l5{grid-area:auto/span 5}.l6{grid-area:auto/span 6}.l7{grid-area:auto/span 7}.l8{grid-area:auto/span 8}.l9{grid-area:auto/span 9}.l10{grid-area:auto/span 10}.l11{grid-area:auto/span 11}.l12{grid-area:auto/span 12}}i,:is(.checkbox,.radio,.switch)>span:before,:is(.checkbox,.radio,.switch)>span>i{--_size: 1.5rem;font-family:var(--font-icon);font-weight:400;font-style:normal;font-size:var(--_size);letter-spacing:normal;text-transform:none;display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;word-wrap:normal;direction:ltr;font-feature-settings:"liga";-webkit-font-smoothing:antialiased;vertical-align:middle;text-align:center;overflow:hidden;inline-size:var(--_size);min-inline-size:var(--_size);block-size:var(--_size);min-block-size:var(--_size);box-sizing:content-box;line-height:normal;border-radius:0}i:has(.badge){overflow:unset}i.tiny{--_size: 1rem}.chip>i,i.small{--_size: 1.25rem}i.medium{--_size: 1.5rem}i.large{--_size: 1.75rem}i.extra{--_size: 2rem}i.fill,a.active>i,button.active>i{font-variation-settings:"FILL" 1}i>:is(img,svg){inline-size:100%;block-size:100%;background-size:100%;border-radius:inherit;position:absolute;inset:0 auto auto 0;padding:inherit}i[class*=fa-]{font-size:calc(var(--_size) * .85);line-height:normal;block-size:auto;min-block-size:auto}.absolute{position:absolute}.fixed{position:fixed}:is(.absolute,.fixed).left.right{inline-size:auto}:is(.absolute,.fixed).left.right.small{block-size:20rem}:is(.absolute,.fixed).left.right.medium{block-size:28rem}:is(.absolute,.fixed).left.right.large{block-size:44rem}:is(.absolute,.fixed).top.bottom.small{inline-size:20rem}:is(.absolute,.fixed).top.bottom.medium{inline-size:28rem}:is(.absolute,.fixed).top.bottom.large{inline-size:44rem}.list{display:flex;flex-direction:column;padding:0;margin:0;flex:1}.list>li,.list>li>details>summary,.list>li>a:only-child{all:unset;box-sizing:border-box;position:relative;display:flex;align-items:center;align-self:normal;text-align:start;justify-content:flex-start;white-space:nowrap;gap:1rem;min-block-size:3.5rem;padding:.5rem 1rem;cursor:pointer;flex:1}.list>li:has(ul,ol,details[open],a:only-child){padding:0}.list>li>.list{padding:0 0 0 1rem}.list>li>*,.list>li>a:only-child>*,.list>li>details>summary>*{margin:0}.list>li>:is(details,.max),.list>li>a:only-child>.max,.list>li>details>summary>.max{flex:1}.list.border>li:not(:last-child):before,.list.border>li>details[open]>summary:before{content:"";position:absolute;background-color:var(--outline-variant);inset:auto 0 0 0;block-size:.0625rem;inline-size:auto}.list.no-space>li,.list.no-space>li>details>summary{min-block-size:2.5rem}.list.medium-space>li,.list.medium-space>li>details>summary{min-block-size:4.5rem}.list.large-space>li,.list.large-space>li>details>summary{min-block-size:5.5rem}:has(>main){display:grid;grid-template-columns:auto 1fr auto;grid-template-rows:auto auto 1fr auto auto;grid-template-areas:"left top right" "left header right" "left main right" "left footer right" "left bottom right";min-block-size:100dvh;box-sizing:border-box;background-color:var(--surface)}nav.left{grid-area:left}nav.right{grid-area:right}nav.top{grid-area:top}nav.bottom{grid-area:bottom}header{grid-area:header}footer{grid-area:footer}main{--_padding: .5rem;grid-area:main;padding:var(--_padding);overflow:hidden}aside{z-index:1}aside:not(.fixed,.absolute).right{float:right}aside:not(.fixed,.absolute).left{float:left}svg{fill:currentcolor}:is(img,svg,video):is(.small,.medium,.large,.tiny,.extra,.round,.circle,.square,.responsive){--_size: 3rem;object-fit:cover;object-position:center;transition:transform var(--speed3),border-radius var(--speed3),padding var(--speed3);block-size:var(--_size);inline-size:var(--_size)}:is(img,svg,video).round{--_round: .5rem}:is(img,svg,video).tiny{--_size: 2rem}:is(img,svg,video).small{--_size: 2.5rem}:is(img,svg,video).large{--_size: 3.5rem}:is(img,svg,video).extra{--_size: 4rem}:is(img,svg,video).responsive{--_size: 100%;margin:0 auto}:is(img,svg,video).responsive.tiny{inline-size:100%;block-size:4rem}:is(img,svg,video).responsive.small{inline-size:100%;block-size:8rem}:is(img,svg,video).responsive.medium{inline-size:100%;block-size:12rem}:is(img,svg,video).responsive.large{inline-size:100%;block-size:16rem}:is(img,svg,video).responsive.extra{inline-size:100%;block-size:20rem}:is(img,svg,video).responsive.round{--_round: 2rem}:is(img,svg,video).empty-state{max-inline-size:100%;inline-size:24rem}:is(button,.button,.chip):not(.transparent)>.responsive{border:.25rem solid transparent}:is(button,.button,.chip,.field)>:is(img,svg):not(.responsive),.tabs :is(img,svg):not(.responsive){min-inline-size:1.5rem;max-inline-size:1.5rem;min-block-size:1.5rem;max-block-size:1.5rem}:is(button,.button,.chip):not(.extend)>.responsive:first-child{margin-inline-start:calc(-1 * var(--_padding))}:is(button,.button,.chip):not(.extend)>.responsive:not(:first-child){margin-inline-end:calc(-1 * var(--_padding))}:is(button,.button,.chip,.circle,.square,.extend)>.responsive{--_size: inherit;margin:0 auto}.extend>:is(.responsive,i){margin:0;position:absolute;inset-inline:1rem;z-index:1}.extend>.responsive{inset-inline:0;inline-size:3.5rem}.extend.border>.responsive{inline-size:3.375rem}menu{opacity:0;visibility:hidden;position:absolute;box-shadow:var(--elevate2);background-color:var(--surface-container);z-index:11;inset:auto auto 0 0;inline-size:100%;max-block-size:50vh;max-inline-size:none!important;overflow-x:hidden;overflow-y:auto;font-size:.875rem;font-weight:400;text-transform:none;color:var(--on-surface);line-height:normal;text-align:start;border-radius:.25rem;transform:scale(.8) translateY(120%);transition:all var(--speed2),0s background-color;justify-content:flex-start;padding:.5rem 0}[dir=rtl] menu{inset:auto 0 0 auto}menu.no-wrap{inline-size:max-content;white-space:nowrap!important}menu.active,:not(menu,[data-ui]):focus-within>menu,menu>li:hover>menu,menu>li>menu:hover{opacity:1;visibility:visible;transform:scale(1) translateY(100%)}menu.active.top,:not(menu,[data-ui]):focus-within>menu.top,menu>li:hover>menu.top,menu>li>menu.top:hover{transform:scale(1) translateY(-100%)}menu *{white-space:inherit!important}menu>li,menu>li>a:only-child{all:unset;box-sizing:border-box;position:relative;display:flex;align-items:center;align-self:normal;text-align:start;justify-content:inherit;white-space:nowrap;gap:1rem;padding:.5rem 1rem;min-block-size:3rem;flex:1;margin:0!important;cursor:pointer}menu>li:is(:hover,:focus,.active){background-color:var(--active)}menu>li>:is(.max,.field),menu>li>a:only-child>.max,menu>li:has(.field,a:only-child){flex:1;padding:0;margin:0}menu.min{inset:0 0 auto 0;transform:none!important;background-color:var(--surface-variant)!important;color:var(--on-surface-variant)!important;padding:0}[dir=rtl] menu.min.right,menu.min.left,menu.top.left{inset:0 0 auto auto}[dir=rtl] menu.min.left,menu.min.right,menu.top,menu.top.right{inset:0 auto auto 0}menu.max{position:fixed;top:0;right:0;bottom:0;left:0;block-size:100%;max-block-size:none;min-block-size:auto;z-index:100;transform:none!important;background-color:var(--surface-variant)!important;color:var(--on-surface-variant)!important;border-radius:0}menu.no-wrap:is(.min,.max){min-inline-size:16rem}[dir=rtl] menu.right,[dir=rtl] menu.top.min.right,menu.left,menu.top.min.left{inset:auto 0 0 auto}[dir=rtl] menu.left,[dir=rtl] menu.top.min.left,menu.right,menu.top.min{inset:auto auto 0 0}menu.top{transform:scale(.8) translateY(-120%)}menu:has(menu){--_child: 1;--_type: 0;overflow:unset;white-space:nowrap;inline-size:auto;min-inline-size:12rem;max-block-size:none}menu>li>:is(menu,menu.right),[dir=rtl] menu>li>menu.left{inset:auto auto calc(3rem * (var(--_child) - var(--_type))) 100%}[dir=rtl] menu>li>:is(menu,menu.right),menu>li>menu.left{inset:auto 100% calc(3rem * (var(--_child) - var(--_type))) auto}menu>li>:is(menu.top,menu.top.right),[dir=rtl] menu>li>menu.top.left{inset:calc(3rem * (var(--_child) - var(--_type))) auto auto 100%}[dir=rtl] menu>li>:is(menu.top,menu.top.right),menu>li>menu.top.left{inset:calc(3rem * (var(--_child) - var(--_type))) 100% auto auto}menu.no-space>li{min-block-size:2.5rem}menu.medium-space>li{min-block-size:3.5rem}menu.large-space>li{min-block-size:4rem}menu.border>li:not(:last-child):before{content:"";position:absolute;background-color:var(--outline-variant);inset:auto 0 0 0;block-size:.0625rem;inline-size:auto}menu.transparent{margin:0 -1rem!important;padding:.5rem}menu.transparent>li{background-color:inherit;box-shadow:none;padding:0}menu>li:nth-last-child(2){--_child: 2}menu>li:nth-last-child(3){--_child: 3}menu>li:nth-last-child(4){--_child: 4}menu>li:nth-last-child(5){--_child: 5}menu>li:nth-last-child(6){--_child: 6}menu>li:nth-last-child(7){--_child: 7}menu>li:nth-last-child(8){--_child: 8}menu>li:nth-last-child(9){--_child: 9}menu>li:nth-last-child(10){--_child: 10}menu>li:nth-last-child(11){--_child: 11}menu>li:nth-last-of-type(2){--_type: 1}menu>li:nth-last-of-type(3){--_type: 2}menu>li:nth-last-of-type(4){--_type: 3}menu>li:nth-last-of-type(5){--_type: 4}menu>li:nth-last-of-type(6){--_type: 5}menu>li:nth-last-of-type(7){--_type: 6}menu>li:nth-last-of-type(8){--_type: 7}menu>li:nth-last-of-type(9){--_type: 8}menu>li:nth-last-of-type(10){--_type: 9}menu>li:nth-last-of-type(11){--_type: 10}@media (pointer: coarse){:not(menu,[data-ui]):hover>menu{opacity:1;visibility:visible;transform:scale(1) translateY(100%)}:not(menu,[data-ui]):hover>menu.top{transform:scale(1) translateY(-100%)}}nav>:is(ol,ul),nav>:is(ol,ul)>li{all:unset}nav,.row,a.row{display:flex;align-items:center;align-self:normal;text-align:start;justify-content:flex-start;white-space:nowrap;gap:1rem;border-radius:0;min-inline-size:0}a.row,nav.row{min-block-size:3rem;margin:0}:is(nav,.row,.max)>:only-child,nav>:is(ol,ul)>li>:only-child{margin:0}:is(nav,.row)>:not(ul,ol,header,footer){margin:0;white-space:normal;flex:none}:is(nav,.row).min{display:inline-flex}:is(nav,.row,li).no-space{gap:0}:is(nav,.row,li).tiny-space{gap:.5rem}:is(nav,.row,li).medium-space{gap:1.5rem}:is(nav,.row,li).large-space{gap:2rem}:is(nav,.row)>.max,:is(nav,.row)>:is(ol,ul)>.max{flex:1}:is(nav,.row).wrap{display:flex;flex-wrap:wrap}:is(header,footer)>:is(nav,.row){min-block-size:inherit}nav:is(.left,.right,.top,.bottom){--_padding: .5rem;--_top: calc(var(--_padding) + var(--top));--_bottom: calc(var(--_padding) + var(--bottom));position:sticky;top:0;right:0;bottom:0;left:0;border:0;color:var(--on-surface);transform:none;z-index:100;text-align:center;padding:var(--_padding);margin:0}nav:is(.left,.right){justify-content:flex-start;flex-direction:column;background-color:var(--surface);block-size:100dvh;min-inline-size:6rem;padding-block:var(--_top) var(--_bottom)}nav:is(.top,.bottom){position:sticky;top:0;right:0;bottom:0;left:0;padding:.5rem;justify-content:center;flex-direction:row;background-color:var(--surface-container);block-size:auto;min-block-size:4.5rem}nav.top{block-size:calc(var(--top) + 4.5rem);padding-block-start:var(--_top)}nav.bottom{block-size:calc(var(--bottom) + 4.5rem);padding-block-end:var(--_bottom)}nav>header{min-block-size:auto;padding:0;margin:0 0 1rem;align-items:flex-start;gap:1rem;background:none!important}nav:is(.top,.bottom)>header{flex-direction:row;align-items:center;margin:0 1rem 0 0}nav>header>*{margin:0}nav>header>.extend:hover{--_padding: 0;inline-size:var(--_size)}nav>header>.extend:hover>span{display:none}nav>:is(ol,ul){all:inherit;min-inline-size:auto;margin:0;padding:0;flex:auto}nav.max:is(.left,.right,.top,.bottom){gap:0;inline-size:auto;align-items:flex-start;min-inline-size:12.75rem;padding:var(--_top) 1.25rem var(--_bottom) 1.25rem}nav.max>:is(ol,ul){padding:0}nav.max>header{margin:0 0 1.25rem}nav.max:is(.top,.bottom)>header{margin:0 1.25rem 0 0}nav.max>header>.extend{--_padding: 1.5rem;inline-size:auto;padding:0 var(--_padding)}nav.max>header>.extend>span{display:block;margin-inline-start:var(--_padding)}nav.max>header>.extend>:is(img,svg)+span{margin-inline-start:calc(1rem + var(--_padding))}nav.max:is(.top,.bottom){padding:0 .5rem;align-items:center;min-inline-size:auto;max-inline-size:none}nav:is(.left,.right,.top,.bottom)>a:not(.button,.chip),nav:is(.left,.right,.top,.bottom)>:is(ol,ul)>li>a:not(.button,.chip){display:flex;flex-direction:column;gap:.25rem;line-height:normal;inline-size:3.5rem;font-size:.8rem}nav:not(.max):is(.left,.right,.top,.bottom)>a:not(.button,.chip)>i,nav:not(.max):is(.left,.right,.top,.bottom)>:is(ol,ul)>li>a:not(.button,.chip)>i{padding:.25rem 1rem;border-radius:2rem;transition:padding var(--speed1) linear;margin:0 auto}nav.max:is(.left,.right,.top,.bottom)>a:not(.button,.chip),nav.max:is(.left,.right,.top,.bottom)>:is(ol,ul)>li>a:not(.button,.chip){flex-direction:row;gap:.5rem;inline-size:auto;block-size:3.5rem;padding:0 1rem;border-radius:2rem;font-size:inherit}nav.max:is(.top,.bottom)>a:not(.button,.chip),nav.max:is(.top,.bottom)>:is(ol,ul)>li>a:not(.button,.chip){gap:.25rem;block-size:2.5rem;font-size:.8rem}nav.max:is(.left,.right,.top,.bottom)>a.active:not(.button,.chip),nav.max:is(.left,.right,.top,.bottom)>:is(ol,ul)>li>a.active:not(.button,.chip),nav:is(.left,.right,.top,.bottom):not(.max)>a.active:not(.button,.chip)>i,nav:is(.left,.right,.top,.bottom):not(.max)>:is(ol,ul)>li>a.active:not(.button,.chip)>i{background-color:var(--secondary-container);color:var(--on-secondary-container)}:is(nav,.row):is(.left-align,.top-align,.vertical){justify-content:flex-start}:is(nav,.row):is(.right-align,.bottom-align){justify-content:flex-end}:is(nav,.row):is(.center-align,.middle-align){justify-content:center}:is(nav,.row):is(.left-align,.top-align,.vertical).vertical{align-items:flex-start}:is(nav,.row):is(.right-align,.bottom-align).vertical{align-items:flex-end}:is(nav,.row):is(.center-align,.middle-align).vertical{align-items:center}nav:not(.left,.right)>.space{inline-size:.5rem}nav:not(.left,.right)>.medium-space{inline-size:1rem}nav:not(.left,.right)>.large-space{inline-size:1.5rem}nav.tabbed{background-color:var(--surface-container);border-radius:4rem!important;gap:0rem;block-size:4rem}nav.tabbed.small{block-size:3rem}nav.tabbed.large{block-size:5rem}nav.tabbed>a{border-radius:inherit;block-size:inherit;display:inline-flex;align-items:center;padding-inline:1rem;gap:.5rem;font-size:1rem;flex:1}nav.tabbed>a.active{background-color:var(--primary-container)}nav.toolbar{display:inline-flex;justify-content:space-around;border-radius:2rem;background-color:var(--surface-container);color:var(--on-surface);padding:0 1rem;gap:.5rem;min-block-size:4rem;min-inline-size:4rem}nav.toolbar>a{display:inline-flex;gap:.5rem;min-inline-size:2.5rem;min-block-size:2.5rem;border-radius:1.75rem}nav.toolbar>a:has(>:not(i)){padding:0 1rem}nav.toolbar>a.active{background-color:var(--secondary-container);color:var(--on-secondary-container)}nav.toolbar.fill{background-color:var(--primary-container)!important;color:var(--on-primary-container)!important}nav.toolbar.fill>a.active{background-color:var(--surface-container)!important;color:var(--on-surface)!important}nav.toolbar.vertical{flex-direction:column!important;min-inline-size:4rem;padding:1rem 0;align-self:center;align-items:center!important}nav.toolbar.vertical>a{inline-size:2.5rem;block-size:2.5rem}nav.toolbar.vertical>a>:is(div,span):not(.badge,.tooltip){display:none}nav.toolbar.max{border-radius:0;display:flex}nav.group{background:none!important}nav.group:is(.connected,.split){gap:.125rem}nav.group:not(.split)>:is(.button,button):not(.border){background-color:var(--surface-container);color:var(--on-surface-container)}nav.group:not(.split)>:is(.button,button).active{background-color:var(--primary);color:var(--on-primary)}nav.group.connected>:is(.button,button):not(.border){background-color:var(--surface-container);color:var(--on-surface-container)}nav.group.connected>:is(.button,button).active{background-color:var(--secondary-container);color:var(--on-secondary-container)}nav.group:is(.connected,.split)>:is(.button,button).active,nav.split>:is(.button,button):active{border-radius:2rem!important}:not(nav)>:is(ul,ol){all:revert}:is(.scroll,.no-scroll,.no-space,.tabs,.tabbed)>:focus-visible{outline:.125rem solid var(--primary);outline-offset:-.125rem}nav.split>:is(.button,button):not(.chip,.fill,.border){background-color:var(--primary);color:var(--on-primary)}nav.group.primary-container>button,nav:is(.max,.toolbar,.tabbed,.group).primary>:is(button,a).active,nav:not(.max,.toolbar,.tabbed,.group).primary>a.active:not(.button,.chip)>i{background-color:var(--primary-container)!important;color:var(--on-primary-container)!important}nav.group.primary>button,nav:is(.max,.toolbar,.tabbed,.group).primary-container>:is(button,a).active,nav:not(.max,.toolbar,.tabbed,.group).primary-container>a.active:not(.button,.chip)>i{background-color:var(--primary)!important;color:var(--on-primary)!important}nav.group.secondary-container>button,nav:is(.max,.toolbar,.tabbed,.group).secondary>:is(button,a).active,nav:not(.max,.toolbar,.tabbed,.group).secondary>a.active:not(.button,.chip)>i{background-color:var(--secondary-container)!important;color:var(--on-secondary-container)!important}nav.group.secondary>button,nav:is(.max,.toolbar,.tabbed,.group).secondary-container>:is(button,a).active,nav:not(.max,.toolbar,.tabbed,.group).secondary-container>a.active:not(.button,.chip)>i{background-color:var(--secondary)!important;color:var(--on-secondary)!important}nav.group.tertiary-container>button,nav:is(.max,.toolbar,.tabbed,.group).tertiary>:is(button,a).active,nav:not(.max,.toolbar,.tabbed,.group).tertiary>a.active:not(.button,.chip)>i{background-color:var(--tertiary-container)!important;color:var(--on-tertiary-container)!important}nav.group.tertiary>button,nav:is(.max,.toolbar,.tabbed,.group).tertiary-container>:is(button,a).active,nav:not(.max,.toolbar,.tabbed,.group).tertiary-container>a.active:not(.button,.chip)>i{background-color:var(--tertiary)!important;color:var(--on-tertiary)!important}@media only screen and (max-width: 600px){nav.top,nav.bottom{justify-content:space-around}}.overlay,dialog::backdrop{display:block!important;opacity:0;visibility:hidden;position:fixed;top:0;right:0;bottom:0;left:0;color:var(--on-surface);background-color:var(--overlay);z-index:100;transition:all var(--speed3),0s background-color;border-radius:0}.overlay.active{opacity:1;visibility:visible}dialog:popover-open::backdrop{opacity:1;visibility:visible}.overlay+dialog::backdrop,.snackbar::backdrop{display:none}[popover]{border:0}.page{--_transform: translate(0, 0);opacity:0;position:absolute;display:none}.page.active{opacity:1;position:inherit;display:inherit;animation:var(--speed4) to-page ease}.page.active.top{--_transform: translate(0, -4rem)}.page.active.bottom{--_transform: translate(0, 4rem)}.page.active.left{--_transform: translate(-4rem, 0)}.page.active.right{--_transform: translate(4rem, 0)}@keyframes to-page{0%{opacity:0;transform:var(--_transform)}to{opacity:1;transform:translate(0)}}progress{--_light: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4AWJiYGAQBgAAAP//ZyYa+wAAAAZJREFUAwAAIgAWeX9MsQAAAABJRU5ErkJggg==);--_dark: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4AWL6//+/FAAAAP//qSv5QwAAAAZJREFUAwAJIAMaJWjIvQAAAABJRU5ErkJggg==);--_size: .25rem;position:relative;inline-size:100%;block-size:var(--_size);color:var(--primary);background:var(--_light);border-radius:1rem;flex:none;border:none;overflow:hidden;writing-mode:horizontal-tb;direction:ltr;-webkit-appearance:none}.dark progress{background:var(--_dark)}progress.small{--_size: .25rem}progress.medium{--_size: .35rem}progress.large{--_size: .45rem}progress:not(.circle,[value]):after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;inline-size:100%;block-size:100%;clip-path:none;background:currentcolor;animation:3.2s to-linear ease infinite}progress:not(.circle,[value])::-moz-progress-bar{animation:3.2s to-linear ease infinite}progress:not(.circle,[value])::-webkit-progress-value{animation:3.2s to-linear ease infinite}progress::-webkit-progress-bar{background:none}progress::-webkit-progress-value{background:currentColor}progress::-moz-progress-bar{background:currentColor}progress.wavy{block-size:calc(var(--_size) * 2);background:none;background-image:var(--_light);background-repeat:repeat-x;background-position:0 50%;background-size:auto calc(var(--_size) / 2)}.dark progress.wavy{background-image:var(--_dark)}progress.wavy::-webkit-progress-value,progress.wavy:not(.circle,[value]):after{-webkit-mask-image:url(wavy.svg);mask-image:url(wavy.svg);-webkit-mask-position:0 50%;mask-position:0 50%;-webkit-mask-repeat:repeat-x;mask-repeat:repeat-x;-webkit-mask-size:auto 100%;mask-size:auto 100%}progress.wavy::-moz-progress-bar{-webkit-mask-image:url(wavy.svg);mask-image:url(wavy.svg);-webkit-mask-position:0 50%;mask-position:0 50%;-webkit-mask-repeat:repeat-x;mask-repeat:repeat-x;-webkit-mask-size:auto 100%;mask-size:auto 100%}progress.circle{display:inline-block;inline-size:2.5rem;block-size:2.5rem;border-radius:50%;border-width:.3rem;border-style:solid;border-color:currentcolor;animation:1.6s to-circular linear infinite;background:none;flex:none}progress.circle::-moz-progress-bar{background:none}progress.circle.small{inline-size:1.5rem;block-size:1.5rem;border-width:.2rem}progress.circle.large{inline-size:3.5rem;block-size:3.5rem;border-width:.4rem}:is(nav,.row,.field)>progress:not(.circle,.small,.medium,.large){flex:auto}progress.max{display:unset;position:absolute;inline-size:100%!important;block-size:100%!important;color:currentColor;background:none;top:0;right:0;bottom:0;left:0;border-radius:inherit;animation:none;writing-mode:horizontal-tb;opacity:.33}progress.max[class*=-text]{opacity:1}progress:is(.horizontal,.vertical,.max){display:unset;inline-size:100%!important}progress.vertical{writing-mode:vertical-lr}progress.max.vertical{transform:rotate(-180deg)}progress.max+*{margin-block-start:0}:is(.button,button,.chip)>progress.circle{color:inherit}@supports (-moz-appearance:none){progress.max.vertical{transform:none}}@keyframes to-linear{0%{margin-inline-start:-100%}50%{margin-inline-start:0%}to{margin-inline-start:100%}}@keyframes to-circular{0%{transform:rotate(0);clip-path:polygon(50% 50%,0% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%)}20%{clip-path:polygon(50% 50%,0% 0%,50% 0%,100% 0%,100% 0%,100% 0%,100% 0%,100% 0%,100% 0%)}30%{clip-path:polygon(50% 50%,0% 0%,50% 0%,100% 0%,100% 50%,100% 50%,100% 50%,100% 50%,100% 50%)}40%{clip-path:polygon(50% 50%,0% 0%,50% 0%,100% 0%,100% 50%,100% 100%,100% 100%,100% 100%,100% 100%)}50%{clip-path:polygon(50% 50%,50% 0%,50% 0%,100% 0%,100% 50%,100% 100%,50% 100%,50% 100%,50% 100%)}60%{clip-path:polygon(50% 50%,100% 50%,100% 50%,100% 50%,100% 50%,100% 100%,50% 100%,0% 100%,0% 100%)}70%{clip-path:polygon(50% 50%,50% 100%,50% 100%,50% 100%,50% 100%,50% 100%,50% 100%,0% 100%,0% 50%)}80%{clip-path:polygon(50% 50%,0% 100%,0% 100%,0% 100%,0% 100%,0% 100%,0% 100%,0% 100%,0% 50%)}90%{transform:rotate(360deg);clip-path:polygon(50% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%)}to{clip-path:polygon(50% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%)}}.shape{display:flex;align-items:center;justify-content:center;color:var(--on-primary);background-color:var(--primary);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-size:contain;mask-size:contain;border-radius:0;block-size:3.5rem;inline-size:3.5rem;margin:0!important;padding:0!important;border:0!important}.transparent>.shape>i{filter:invert(1)}.shape.tiny-space{-webkit-mask-size:90%;mask-size:90%}.shape.space,.shape.small-space{-webkit-mask-size:80%;mask-size:80%}.shape.medium-space{-webkit-mask-size:70%;mask-size:70%}.shape.large-space{-webkit-mask-size:60%;mask-size:60%}.shape.extra-space{-webkit-mask-size:50%;mask-size:50%}.shape.tiny{block-size:2.5rem;inline-size:2.5rem}.shape.medium{block-size:4.5rem;inline-size:4.5rem}.shape.large{block-size:5.5rem;inline-size:5.5rem}.shape.extra{block-size:6.5rem;inline-size:6.5rem}.shape.max,.shape>.responsive,.shape>.responsive>.responsive{position:absolute;top:0;right:0;bottom:0;left:0;block-size:100%;inline-size:100%;margin:0!important;padding:0!important;border:0!important}.shape>.responsive{background:inherit;color:inherit}.shape.rotate{animation:linear to-shape-rotate infinite 12s}.shape.rotate>*{animation:linear to-shape-rotate infinite 12s reverse}.shape.fast-rotate{animation:linear to-shape-rotate infinite 6s}.shape.fast-rotate>*{animation:linear to-shape-rotate infinite 6s reverse}.shape.slow-rotate{animation:linear to-shape-rotate infinite 24s}.shape.slow-rotate>*{animation:linear to-shape-rotate infinite 24s reverse}:is(button,.button,.chip):has(>.shape)>.responsive{border:none}.shape.arch{-webkit-mask-image:url(arch.svg);mask-image:url(arch.svg)}.shape.arrow{-webkit-mask-image:url(arrow.svg);mask-image:url(arrow.svg)}.shape.boom{-webkit-mask-image:url(boom.svg);mask-image:url(boom.svg)}.shape.bun{-webkit-mask-image:url(bun.svg);mask-image:url(bun.svg)}.shape.burst{-webkit-mask-image:url(burst.svg);mask-image:url(burst.svg)}.shape.circle{-webkit-mask-image:url(circle.svg);mask-image:url(circle.svg)}.shape.clamshell{-webkit-mask-image:url(clamshell.svg);mask-image:url(clamshell.svg)}.shape.diamond{-webkit-mask-image:url(diamond.svg);mask-image:url(diamond.svg)}.shape.fan{-webkit-mask-image:url(fan.svg);mask-image:url(fan.svg)}.shape.flower{-webkit-mask-image:url(flower.svg);mask-image:url(flower.svg)}.shape.gem{-webkit-mask-image:url(gem.svg);mask-image:url(gem.svg)}.shape.ghost-ish{-webkit-mask-image:url(ghost-ish.svg);mask-image:url(ghost-ish.svg)}.shape.heart{-webkit-mask-image:url(heart.svg);mask-image:url(heart.svg)}.shape.leaf-clover4{-webkit-mask-image:url(leaf-clover4.svg);mask-image:url(leaf-clover4.svg)}.shape.leaft-clover8{-webkit-mask-image:url(leaf-clover8.svg);mask-image:url(leaf-clover8.svg)}.shape.loading-indicator{-webkit-mask-image:url(loading-indicator.svg);mask-image:url(loading-indicator.svg)}.shape.oval{-webkit-mask-image:url(oval.svg);mask-image:url(oval.svg)}.shape.pentagon{-webkit-mask-image:url(pentagon.svg);mask-image:url(pentagon.svg)}.shape.pill{-webkit-mask-image:url(pill.svg);mask-image:url(pill.svg)}.shape.pixel-circle{-webkit-mask-image:url(pixel-circle.svg);mask-image:url(pixel-circle.svg)}.shape.pixel-triangle{-webkit-mask-image:url(pixel-triangle.svg);mask-image:url(pixel-triangle.svg)}.shape.puffy{-webkit-mask-image:url(puffy.svg);mask-image:url(puffy.svg)}.shape.puffy-diamond{-webkit-mask-image:url(puffy-diamond.svg);mask-image:url(puffy-diamond.svg)}.shape.semicircle{-webkit-mask-image:url(semicircle.svg);mask-image:url(semicircle.svg)}.shape.sided-cookie4{-webkit-mask-image:url(sided-cookie4.svg);mask-image:url(sided-cookie4.svg)}.shape.sided-cookie6{-webkit-mask-image:url(sided-cookie6.svg);mask-image:url(sided-cookie6.svg)}.shape.sided-cookie7{-webkit-mask-image:url(sided-cookie7.svg);mask-image:url(sided-cookie7.svg)}.shape.sided-cookie9{-webkit-mask-image:url(sided-cookie9.svg);mask-image:url(sided-cookie9.svg)}.shape.sided-cookie12{-webkit-mask-image:url(sided-cookie12.svg);mask-image:url(sided-cookie12.svg)}.shape.slanted{-webkit-mask-image:url(slanted.svg);mask-image:url(slanted.svg)}.shape.soft-boom{-webkit-mask-image:url(soft-boom.svg);mask-image:url(soft-boom.svg)}.shape.soft-burst{-webkit-mask-image:url(soft-burst.svg);mask-image:url(soft-burst.svg)}.shape.square{-webkit-mask-image:url(square.svg);mask-image:url(square.svg)}.shape.sunny{-webkit-mask-image:url(sunny.svg);mask-image:url(sunny.svg)}.shape.triangle{-webkit-mask-image:url(triangle.svg);mask-image:url(triangle.svg)}.shape.very-sunny{-webkit-mask-image:url(very-sunny.svg);mask-image:url(very-sunny.svg)}@keyframes to-shape-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.checkbox,.radio,.switch{--_size: 1.5rem;direction:ltr;inline-size:auto;block-size:auto;line-height:normal;white-space:nowrap;cursor:pointer;display:inline-flex;align-items:center}:is(.checkbox,.radio,.switch).small{--_size: 1rem}:is(.checkbox,.radio,.switch).large{--_size: 2rem}:is(.checkbox,.radio,.switch).extra{--_size: 2.5rem}:is(.checkbox,.radio)>input{inline-size:var(--_size);block-size:var(--_size);opacity:0}.switch>input{inline-size:3.25rem;block-size:2rem;opacity:0}:is(.checkbox,.radio,.switch)>span{display:inline-flex;align-items:center;color:var(--on-surface);font-size:.875rem}:is(.checkbox,.radio)>span:not(:empty){padding-inline-start:.25rem}:is(.checkbox,.radio,.switch)>span:before,:is(.checkbox,.radio,.switch)>span>i,:is(.checkbox,.radio)>span:after{--_size: inherit;content:"";inline-size:var(--_size);block-size:var(--_size);box-sizing:border-box;margin:0 auto;outline:none;color:var(--primary);position:absolute;inset:auto auto auto calc(var(--_size) * -1);border-radius:50%;-webkit-user-select:none;user-select:none;z-index:1}.switch>span:before,.switch.icon>span>i{position:absolute;inset:50% auto auto 0;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;transition:all var(--speed2);font-size:calc(var(--_size) - .5rem);-webkit-user-select:none;user-select:none;min-inline-size:var(--_size);min-block-size:var(--_size);content:"";color:var(--surface-variant);background-color:var(--outline)}.switch>span:before,.switch.icon>span>i{transform:translate(-3rem,-50%) scale(.6)}.switch.icon>span>i{transform:translate(-3rem,-50%) scale(1)}.checkbox>span:before{content:"check_box_outline_blank"}.checkbox>input:checked+span:before{content:"check_box";font-variation-settings:"FILL" 1}.checkbox>input:indeterminate+span:before{content:"indeterminate_check_box"}.radio>span:before{content:"radio_button_unchecked"}.radio>input:checked+span:before{content:"radio_button_checked"}:is(.radio,.checkbox,.switch).icon>span:before{content:""!important;font-variation-settings:unset!important}:is(.checkbox,.radio)>span:after{transition:all var(--speed1);background-color:currentColor;box-shadow:0 0 0 0 currentColor;opacity:0}:is(.checkbox,.radio):is(:hover)>input:not(:disabled)+span:after,:is(.checkbox,.radio)>input:not(:disabled):is(:focus)+span:after{box-shadow:0 0 0 .5rem currentColor;opacity:.1}.switch>input:not(:disabled):is(:focus,:hover)+span:before,.switch.icon>input:not(:disabled):is(:focus,:hover)+span>i{box-shadow:0 0 0 .5rem var(--active)}:is(.checkbox,.radio)>input:checked+span:before,:is(.checkbox,.radio).icon>input:checked+span>i{color:var(--primary)}.icon>input:checked+span>i:first-child,.icon>span>i:last-child{opacity:0}.icon>input:checked+span>i:last-child,.icon>span>i:first-child{opacity:1}.switch>input:checked+span:after{border:none;background-color:var(--primary)}.switch>input:checked+span:before,.switch.icon>input:checked+span>i{content:"check";color:var(--primary);background-color:var(--on-primary);transform:translate(-1.75rem,-50%) scale(1)}.switch>input:active:not(:disabled)+span:before,.switch.icon>input:active:not(:disabled)+span>i{transform:translate(-3rem,-50%) scale(1.2)}.switch>input:active:checked:not(:disabled)+span:before,.switch.icon>input:active:checked:not(:disabled)+span>i{transform:translate(-1.75rem,-50%) scale(1.2)}:is(.checkbox,.radio,.switch)>input:disabled+span{opacity:.5;cursor:not-allowed}.switch>span:after{content:"";position:absolute;inset:50% auto auto 0;background-color:var(--active);border:.125rem solid var(--outline);box-sizing:border-box;inline-size:3.25rem;block-size:2rem;border-radius:2rem;transform:translate(-3.25rem,-50%)}.field>:is(nav,.row){flex-grow:1;padding:0 1rem}.field.round>:is(nav,.row){flex-grow:1;padding:0 1.5rem}[dir=rtl] .switch{transform:scale(-1)}[dir=rtl] .switch>span:before,[dir=rtl] .switch.icon>span>i{transform:translate(-3rem,-50%) scale(-.6)}[dir=rtl] .switch.icon>span>i{transform:translate(-3rem,-50%) scale(-1)}[dir=rtl] .switch>input:checked+span:before,[dir=rtl] .switch.icon>input:checked+span>i{transform:translate(-1.75rem,-50%) scale(-1)}.switch>:focus-visible+span:after{outline:.125rem solid var(--primary);outline-offset:.25rem}:is(.checkbox,.radio)>:focus-visible+span:before{outline:.125rem solid var(--primary);outline-offset:.375rem}.slider{--_start: 0%;--_end: 0%;--_value1: "";--_value2: "";--_track: 1rem;--_thumb: max(2.5rem, calc(var(--_track) + .5rem));display:flex;align-items:center!important;inline-size:auto;block-size:var(--_thumb);flex:none;direction:ltr;margin:0 1.25rem}[dir=rtl] .slider{transform:scaleX(-1)}.slider.vertical{flex-direction:row!important;margin:.5rem auto!important;padding:50% 0;transform:rotate(-90deg);inline-size:100%}.slider.tiny{--_track: 1rem}.slider.small{--_track: 1.5rem}.slider.medium{--_track: 2.5rem}.slider.large{--_track: 3.5rem}.slider.extra{--_track: 6rem}.slider>input{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-shadow:none;border:none;outline:none;pointer-events:none;inline-size:100%;block-size:var(--_track);background:none;z-index:1;padding:0;margin:0;transform:rotate(0)}.slider>input:only-of-type{pointer-events:all}.slider>input+input{position:absolute}.slider>input::-webkit-slider-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-shadow:none;border:none;outline:none;pointer-events:all;block-size:var(--_thumb);inline-size:.25rem;border-radius:.25rem;background:var(--primary);cursor:grab;margin:0;z-index:1}.slider>input::-webkit-slider-thumb:active{cursor:grabbing}.slider>input::-moz-range-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-shadow:none;border:none;outline:none;pointer-events:all;block-size:2.75rem;inline-size:.25rem;border-radius:.25rem;background:var(--primary);cursor:grab;margin:0}.slider>input::-moz-range-thumb:active{cursor:grabbing}.slider>input:not(:disabled):is(:focus)::-webkit-slider-thumb{transform:scaleX(.6)}.slider>input:not(:disabled):is(:focus)::-moz-range-thumb{transform:scaleX(.6)}.slider>input:disabled{cursor:not-allowed;opacity:1}.slider>input:disabled::-webkit-slider-thumb{background:var(--outline);cursor:not-allowed}.slider>input:disabled::-moz-range-thumb{background:var(--outline);cursor:not-allowed}.slider>input:disabled~span{background:var(--outline)}.slider>span{position:absolute;block-size:var(--_track);border-radius:1rem 0 0 1rem;background:var(--primary);color:var(--on-primary);z-index:0;inset:calc(50% - (var(--_track) / 2)) var(--_end) auto var(--_start);clip-path:polygon(0 0,calc(100% - .5rem) 0,calc(100% - .5rem) 100%,0 100%)}.slider>input[type=range]+input[type=range]~span{border-radius:0;clip-path:polygon(.5rem 0,max(.5rem,calc(100% - .5rem)) 0,max(.5rem,calc(100% - .5rem)) 100%,.5rem 100%)}.field>.slider{inline-size:100%}.slider:before{content:"";position:absolute;inline-size:100%;block-size:var(--_track);border-radius:1rem;background:var(--secondary-container);clip-path:polygon(calc(var(--_start) - .5rem) 0,0 0,0 100%,calc(var(--_start) - .5rem) 100%,calc(var(--_start) - .5rem) 0,calc(100% - var(--_end) + .5rem) 0,100% 0,100% 100%,calc(100% - var(--_end) + .5rem) 100%,calc(100% - var(--_end) + .5rem) 0)}.slider:has(>[disabled]):before{background:var(--outline-variant)}.slider:has([disabled]){opacity:.62}.slider>span>i{position:absolute;block-size:auto;inset:0 auto 0 .5rem;color:currentColor;z-index:1}.slider:not(.medium,.large,.extra)>span>i{display:none}.slider.vertical>i{transform:rotate(90deg)}.slider>.tooltip{visibility:hidden!important;opacity:0!important;inset:0 auto auto calc(100% - var(--_end));border-radius:2rem;transition:top var(--speed2) ease,opacity var(--speed2) ease;transform:translate(-50%,-25%)!important;padding:.75rem 1rem}.slider>.tooltip.bottom{inset:auto auto 0 calc(100% - var(--_end));transition:bottom var(--speed2) ease,opacity var(--speed2) ease;transform:translate(-50%,25%)!important}[dir=rtl] .slider>.tooltip{transform:translate(-50%,-25%) scaleX(-1)!important}[dir=rtl] .slider>.tooltip.bottom{transform:translate(-50%,25%) scaleX(-1)!important}.slider>.tooltip+.tooltip{inset:.25rem calc(100% - var(--_start)) auto auto;transform:translate(50%,-25%)!important}.slider>.tooltip+.tooltip.bottom{inset:auto calc(100% - var(--_start)) -.25rem auto;transform:translate(50%,25%)!important}[dir=rtl] .slider>.tooltip+.tooltip{transform:translate(50%,-25%) scaleX(-1)!important}[dir=rtl] .slider>.tooltip+.tooltip.bottom{transform:translate(50%,25%) scaleX(-1)!important}.slider>.tooltip:before{content:var(--_value1)}.slider>.tooltip+.tooltip:before{content:var(--_value2)}.slider>:focus~.tooltip{inset-block:-1rem auto;opacity:1!important;visibility:visible!important}.slider>:focus~.tooltip.bottom{inset-block:auto -1rem}.slider.vertical>.tooltip{inset-block:auto;block-size:2.5rem;inline-size:2.5rem;margin-block:calc(-1 * var(--_thumb)) 0!important;transform:rotate(90deg) translate(-75%,50%)!important}.slider.vertical>.tooltip.bottom{inset-block:auto;margin-block:0 calc(-1 * var(--_thumb))!important;transform:rotate(90deg) translate(75%,50%)!important}.slider.vertical>.tooltip+.tooltip{transform:rotate(90deg) translate(-75%,-50%)!important}.slider.vertical>.tooltip+.tooltip.bottom{transform:rotate(90deg) translate(75%,-50%)!important}:is(nav,.row,.field)>.slider:not(.circle,.small,.medium,.large){flex:auto}.slider.max,.slider.max.vertical,.slider.max>input,.slider.max.vertical>input{all:unset;margin:0!important;position:absolute;color:var(--primary);top:0;right:0;bottom:0;left:0;border-radius:inherit;overflow:hidden;z-index:2;cursor:grab;inline-size:100%;block-size:100%}.slider.max:before{display:none}.slider.max.vertical>input{writing-mode:vertical-lr;transform:rotate(-180deg)}.slider.max>input::-webkit-slider-thumb{opacity:0;inline-size:1rem;block-size:100vh;transform:none!important}.slider.max>input::-moz-range-thumb{opacity:0;inline-size:1rem;block-size:100vh;transform:none!important}.slider.max>span{block-size:auto!important;inset:0 var(--_end) 0 var(--_start);clip-path:none;background:currentcolor;color:inherit;border-radius:0}.slider.max.vertical>span{inset:var(--_end) 0 var(--_start) 0}.slider>input:focus-visible::-webkit-slider-thumb{outline:.1875rem solid var(--primary);outline-offset:.25rem}.slider>input:focus-visible::-moz-range-thumb{outline:.1875rem solid var(--primary);outline-offset:.25rem}.slider.max>input:focus-visible{outline:.1875rem solid var(--primary);outline-offset:-.125rem}@media (pointer: coarse){.slider>:hover~.tooltip{inset-block:-1rem auto!important;opacity:1!important;visibility:visible!important}.slider>:hover~.tooltip.bottom{inset-block:auto -1rem!important}}.snackbar{position:fixed;inset:auto auto 6rem 50%;inline-size:80%;block-size:auto;z-index:200;visibility:hidden;display:flex;box-shadow:var(--elevate2);color:var(--inverse-on-surface);background-color:var(--inverse-surface);padding:1rem;cursor:pointer;text-align:start;align-items:center;border-radius:.25rem;gap:.5rem;transition:all var(--speed2);transform:translate(-50%,1rem);opacity:0}.snackbar.top{inset:6rem auto auto 50%}.snackbar:is(.active){visibility:visible;transform:translate(-50%);opacity:1}.snackbar:popover-open{visibility:visible;transform:translate(-50%);opacity:1}.snackbar>.max{flex:auto}@media only screen and (min-width: 993px){.snackbar{inline-size:40%}}table{inline-size:100%;border-spacing:0;font-size:.875rem;text-align:start}.scroll>table,table :is(thead,tbody,tfoot,tr,th,td){background-color:inherit;color:inherit}:is(th,td){inline-size:auto;text-align:inherit;padding:.5rem;border-radius:0}:is(th,td)>*{vertical-align:middle}table.border>tbody>tr:not(:last-child)>td,thead>tr>th{border-block-end:.0625rem solid var(--outline)}tfoot>tr>th{border-block-start:.0625rem solid var(--outline)}table.stripes>tbody>tr:nth-child(odd){background-color:var(--active)}table.no-space :is(th,td){padding:0}table.medium-space :is(th,td){padding:.75rem}table.large-space :is(th,td){padding:1rem}table>.fixed,th.fixed{position:sticky;z-index:1;inset-block-start:0}tfoot.fixed,tfoot th.fixed{inset-block-end:0}:is(td,th).min{inline-size:.1%;white-space:nowrap}.tabs{display:flex;white-space:nowrap;border-block-end:.0625rem solid var(--surface-variant);border-radius:0}.tabs:not(.left-align,.right-align,.center-align){justify-content:space-around}.tabs>a{display:flex;font-size:.875rem;font-weight:500;color:var(--on-surface-variant);padding:.5rem 1rem;text-align:center;min-block-size:3rem;inline-size:100%;gap:.25rem}.tabs.small>a{min-block-size:2rem}.tabs.large>a{min-block-size:4rem}.tabs>a.active,.tabs>a.active>i{color:var(--primary)}.tabs>a.active:before{content:"";position:absolute;inset:auto 0 0 0;block-size:.125rem;background-color:var(--primary)}.tabs.min>a.active:before{margin:0 auto;max-inline-size:min(100%,4rem)}.tabs:is(.left-align,.center-align,.right-align)>a{inline-size:auto}.tooltip{--_space: -.5rem;visibility:hidden;display:flex;align-items:center;justify-content:center;gap:.5rem;background-color:var(--inverse-surface);color:var(--inverse-on-surface);font-size:.75rem;text-align:center;border-radius:.25rem;padding:.5rem;position:absolute;z-index:200;inset:0 auto auto 50%;inline-size:auto;white-space:nowrap;font-weight:500;opacity:0;transition:all var(--speed2);line-height:normal;transform:translate(-50%,-100%) scale(.9)}.tooltip:not(.max):hover{visibility:hidden;opacity:0}.tooltip.left{inset:50% auto auto 0;transform:translate(-100%,-50%) scale(.9)}.tooltip.right{inset:50% 0 auto auto;transform:translate(100%,-50%) scale(.9)}.tooltip.bottom{inset:auto auto 0 50%;transform:translate(-50%,100%) scale(.9)}.tooltip.small{inline-size:8rem;white-space:normal}.tooltip.medium{inline-size:12rem;white-space:normal}.tooltip.large{inline-size:16rem;white-space:normal}:hover>.tooltip{visibility:visible;opacity:1;transform:translate(-50%,-100%) scale(1)}:hover>.tooltip.left{transform:translate(-100%,-50%) scale(1)}:hover>.tooltip.right{transform:translate(100%,-50%) scale(1)}:hover>.tooltip.bottom{transform:translate(-50%,100%) scale(1)}.tooltip.no-space{--_space: 0}.tooltip.medium-space{--_space: -1rem}.tooltip.large-space{--_space: -1.5rem}.tooltip:not(.left,.right,.bottom){margin-block-start:var(--_space)!important}.tooltip.left,.tooltip.right{margin-inline:var(--_space)!important}.tooltip.bottom{margin-block-end:var(--_space)!important}menu:active~.tooltip,:is(button,.button):focus>menu~.tooltip,.field>:focus~menu~.tooltip{visibility:hidden}.slider>.tooltip{--_space: -1.25rem}.slider.vertical>.tooltip{--_space: -.75rem}.slider.vertical>.tooltip:is(.left,.right){--_space: -.5rem}.tooltip.max{display:block;font-size:inherit;white-space:normal;text-align:start;inline-size:20rem;border-radius:.5rem;padding:1rem;box-shadow:var(--elevate2)} diff --git a/Tesses.YouTubeDownloader.Server/res/beer.min.js b/Tesses.YouTubeDownloader.Server/res/beer.min.js new file mode 100644 index 0000000..06abc85 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/beer.min.js @@ -0,0 +1 @@ +const U=[];function Z(){return window==null?void 0:window.matchMedia("(pointer: coarse)").matches}function lt(){return window==null?void 0:window.matchMedia("(prefers-color-scheme: dark)").matches}async function W(t){await new Promise(n=>setTimeout(n,t))}function ft(){return"fxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const n=Math.random()*16|0;return(t==="x"?n:n&3|8).toString(16)})}function h(t,n){try{return typeof t=="string"?(n??document).querySelector(t):t}catch{return null}}function u(t,n){try{return typeof t=="string"?(n??document).querySelectorAll(t):t??U}catch{return U}}function a(t,n){return(t==null?void 0:t.classList.contains(n))??!1}function y(t,n){var e;return((e=t==null?void 0:t.tagName)==null?void 0:e.toLowerCase())===n}function T(t,n){var e;return((e=t==null?void 0:t.type)==null?void 0:e.toLowerCase())===n}function g(t,n){if(t instanceof NodeList)for(let e=0;e label");for(let n=0;n input:not([type=file], [type=color], [type=range])");for(let n=0;n select");for(let n=0;n input[type=file]");for(let n=0;n input[type=color]");for(let n=0;n textarea");for(let n=0;ni.name).join(", "):"",e.readOnly=!0,o(e,"keydown",kt,!1),L(e))}function R(t,n){if((n==null?void 0:n.key)==="Enter"){const i=_(t);if(!T(i,"color"))return;i.click();return}const e=O(t);T(e,"text")&&(e.readOnly=!0,e.value=t.value,o(e,"keydown",mt,!1),L(e))}function Y(t){X(t);const n=v(t);n.removeAttribute("style"),a(n,"min")&&n.style.setProperty("--_size",`${Math.max(t.scrollHeight,n.offsetHeight)}px`)}function Mt(){Tt(),Lt(),At(),Et(),St(),Dt(),It()}function V(t){const n=t.target;!y(n,"input")&&!y(n,"select")||(n.type==="range"?(n.focus(),Q(n)):G())}function Pt(t){if(!Z())return;const n=t.target,e=v(n);a(e,"vertical")&&document.body.classList.add("no-scroll")}function _t(t){if(!Z())return;const n=t.target,e=v(n);a(e,"vertical")&&document.body.classList.remove("no-scroll")}function G(){const t=document.body,n=u(".slider > input[type=range]");n.length?o(t,"input",V,!1):P(t,"input",V,!1);for(let e=0;e1&&(f=Math.abs(r[1]-r[0]),b=r[1]>r[0]?r[0]:r[1],A=100-b-f,w>k&&(k=l[1]||0,w=l[0])),n.style.setProperty("--_start",`${b}%`),n.style.setProperty("--_end",`${A}%`),n.style.setProperty("--_value1",`'${k}'`),n.style.setProperty("--_value2",`'${w}'`)}function Ft(){G()}const d={light:"",dark:""};function M(){var t;return(t=document==null?void 0:document.body)!=null&&t.classList.contains("dark")?"dark":"light"}function $t(){if(d.light&&d.dark)return d;const t=document.body,n=document.createElement("body");n.className="light",t.appendChild(n);const e=document.createElement("body");e.className="dark",t.appendChild(e);const i=getComputedStyle(n),s=getComputedStyle(e),c=["--primary","--on-primary","--primary-container","--on-primary-container","--secondary","--on-secondary","--secondary-container","--on-secondary-container","--tertiary","--on-tertiary","--tertiary-container","--on-tertiary-container","--error","--on-error","--error-container","--on-error-container","--background","--on-background","--surface","--on-surface","--surface-variant","--on-surface-variant","--outline","--outline-variant","--shadow","--scrim","--inverse-surface","--inverse-on-surface","--inverse-primary","--surface-dim","--surface-bright","--surface-container-lowest","--surface-container-low","--surface-container","--surface-container-high","--surface-container-highest"];for(let r=0,l=c.length;r{const c=r=>{let l="";for(let f=0,b=Object.keys(r),A=b.length;f{const n=h(".field > input",t);n?n.focus():t.focus()},90)}function ot(t,n,e){S&&clearTimeout(S),S=setTimeout(()=>{o(document.body,"click",it),y(document.activeElement,"input")||F();const i=a(n,"active"),s=(e==null?void 0:e.target)===t,c=!!t.closest("menu");if(!i&&c||i&&s){p(n,"active");return}p(u("menu.active"),"active"),g(n,"active"),Bt(n)},90)}let C;function jt(t){const n=t.currentTarget;p(n,"active"),C&&clearTimeout(C)}function Kt(t,n){F();const e=u(".snackbar.active");for(let i=0;i{p(t,"active")},n??6e3))}function Ut(t){const n=v(t);n&&p(u(":scope > .page",n),"active"),g(t,"active")}function Vt(t){Ht(t)}function Ht(t){const n=t.currentTarget,e=n.getBoundingClientRect(),i=Math.max(e.width,e.height),s=i/2,c=t.clientX-e.left-s,r=t.clientY-e.top-s,l=document.createElement("div");l.className="ripple-js";const f=document.createElement("div");f.style.inlineSize=f.style.blockSize=`${i}px`,f.style.left=`${c}px`,f.style.top=`${r}px`,f.addEventListener("animationend",()=>{l.remove()}),l.appendChild(f),n.appendChild(l)}function Zt(){const t=u(".slow-ripple, .ripple, .fast-ripple");for(let n=0;nawait ct(),180)}async function q(t,n,e,i){if(!n&&(n=h(t.getAttribute("data-ui")),!n)){t.classList.toggle("active");return}if(ht(t),y(n,"dialog")){await et(t,n);return}if(y(n,"menu")){ot(t,n,i);return}if(a(n,"snackbar")){Kt(n,e);return}if(a(n,"page")){Ut(n);return}if(a(n,"active")){p(t,"active"),p(n,"active");return}g(n,"active")}function Wt(t){q(t.currentTarget,null,null,t)}function Xt(t){t.key==="Enter"&&q(t.currentTarget,null,null,t)}function rt(){m.ui||I||!m.MutationObserver||(I=new MutationObserver(H),I.observe(document.body,{childList:!0,subtree:!0}),H())}function Yt(){const t=u("[data-ui]");for(let n=0,e=t.length;n=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;q(t,function(e){return!!(r=o(t,ce(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function A(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function L(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function N(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){R(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=A(t);let r;if(n==="html"){r=new DocumentFragment;const i=L(e);N(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=L(t);N(r,i.body);r.title=i.title}else{const i=L('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function D(e){return typeof e==="function"}function k(e){return t(e,"Object")}function oe(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function F(t){const n=[];if(t){for(let e=0;e=0}function se(e){return e.getRootNode({composed:true})===document}function X(e){return e.trim().split(/\s+/)}function le(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function v(e){try{return JSON.parse(e)}catch(e){R(e);return null}}function B(){const e="htmx:sessionStorageTest";try{sessionStorage.setItem(e,e);sessionStorage.removeItem(e);return true}catch(e){return false}}function U(e){const t=new URL(e,"http://x");if(t){e=t.pathname+t.search}if(e!="/"){e=e.replace(/\/+$/,"")}return e}function e(e){return On(te().body,function(){return eval(e)})}function V(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function j(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function $(){Q.logger=null}function f(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return f(te(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(te(),e)}}function b(){return window}function _(e,t){e=w(e);if(t){b().setTimeout(function(){_(e);e=null},t)}else{u(e).removeChild(e)}}function ce(e){return e instanceof Element?e:null}function z(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function p(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ce(w(e));if(!e){return}if(n){b().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ce(w(e));if(!r){return}if(n){b().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=w(e);e.classList.toggle(t)}function Z(e,t){e=w(e);ie(e.parentElement.children,function(e){G(e,t)});K(ce(e),t)}function g(e,t){e=ce(w(e));if(e){return e.closest(t)}return null}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function pe(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(t,r,n){if(r.indexOf("global ")===0){return m(t,r.slice(7),true)}t=w(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=pe(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ce(t),pe(r.slice(8)))}else if(r.indexOf("find ")===0){e=f(p(t),pe(r.slice(5)))}else if(r==="next"||r==="nextElementSibling"){e=ce(t).nextElementSibling}else if(r.indexOf("next ")===0){e=ge(t,pe(r.slice(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ce(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,pe(r.slice(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=y(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=p(y(t,!!n));i.push(...F(c.querySelectorAll(e)))}return i}var ge=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ue(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(te().body,e)[0]}}function w(e,t){if(typeof e==="string"){return f(p(t)||document,e)}else{return e}}function ye(e,t,n,r){if(D(t)){return{target:te().body,event:J(e),listener:t,options:n}}else{return{target:w(e),event:J(t),listener:n,options:r}}}function xe(t,n,r,o){Gn(function(){const e=ye(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=D(n);return e?n:r}function be(t,n,r){Gn(function(){const e=ye(t,n,r);e.target.removeEventListener(e.event,e.listener)});return D(n)?n:r}const ve=te().createElement("output");function we(t,n){const e=ne(t,n);if(e){if(e==="this"){return[Se(t,n)]}else{const r=m(t,e);const o=/(^|,)(\s*)inherit(\s*)($|,)/.test(e);if(o){const i=ce(q(t,function(e){return e!==t&&s(ce(e),n)}));if(i){r.push(...we(i,n))}}if(r.length===0){R('The selector "'+e+'" on '+n+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ce(q(e,function(e){return a(ce(e),t)!=null}))}function Ee(e){const t=ne(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ue(e,t)}}else{const n=oe(e);if(n.boosted){return te().body}else{return e}}}function Ce(e){return Q.config.attributesToSettle.includes(e)}function Oe(t,n){ie(Array.from(t.attributes),function(e){if(!n.hasAttribute(e.name)&&Ce(e.name)){t.removeAttribute(e.name)}});ie(n.attributes,function(e){if(Ce(e.name)){t.setAttribute(e.name,e.value)}})}function Re(t,e){const n=Jn(e);for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=m(t,n,false);if(r.length){ie(r,function(e){let t;const n=o.cloneNode(true);t=te().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=p(n)}const r={shouldSwap:true,target:e,fragment:t};if(!ae(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);$e(s,e,e,t,i);Te()}ie(i.elts,function(e){ae(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(te().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=f("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=f("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){ie(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=a(e,"id");const n=te().getElementById(t);if(n!=null){if(e.moveBefore){let e=f("#--htmx-preserve-pantry--");if(e==null){te().body.insertAdjacentHTML("afterend","
");e=f("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Ae(l,e,c){ie(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=p(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Le(e){return function(){G(e,Q.config.addedClass);Mt(ce(e));Ne(p(e));ae(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=z(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function c(e,t,n,r){Ae(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ce(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Le(o))}}}function Ie(e,t){let n=0;while(n0}function ze(h,d,p,g){if(!g){g={}}let m=null;let n=null;let e=function(){re(g.beforeSwapCallback);h=w(h);const r=g.contextElement?y(g.contextElement,false):te();const e=document.activeElement;let t={};t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};const o=Sn(h);if(p.swapStyle==="textContent"){h.textContent=d}else{let n=P(d);o.title=g.title||n.title;if(g.historyRequest){n=n.querySelector("[hx-history-elt],[data-hx-history-elt]")||n}if(g.selectOOB){const i=g.selectOOB.split(",");for(let t=0;t0){b().setTimeout(n,p.settleDelay)}else{n()}};let t=Q.config.globalViewTransitions;if(p.hasOwnProperty("transition")){t=p.transition}const r=g.contextElement||te();if(t&&ae(r,"htmx:beforeTransition",g.eventInfo)&&typeof Promise!=="undefined"&&document.startViewTransition){const o=new Promise(function(e,t){m=e;n=t});const i=e;e=function(){document.startViewTransition(function(){i();return o})}}try{if(p?.swapDelay&&p.swapDelay>0){b().setTimeout(e,p.swapDelay)}else{e()}}catch(e){fe(r,"htmx:swapError",g.eventInfo);re(n);throw e}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=v(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(k(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}ae(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=On(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(te().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function O(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=O(e,Qe).trim();e.shift()}else{t=O(e,E)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{O(o,C);const l=o.length;const c=O(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};O(o,C);u.pollInterval=d(O(o,/[,\[\s]/));O(o,C);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const f={trigger:c};var i=nt(e,o,"event");if(i){f.eventFilter=i}O(o,C);while(o.length>0&&o[0]!==","){const a=o.shift();if(a==="changed"){f.changed=true}else if(a==="once"){f.once=true}else if(a==="consume"){f.consume=true}else if(a==="delay"&&o[0]===":"){o.shift();f.delay=d(O(o,E))}else if(a==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=O(o,E);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}f.from=s}else if(a==="target"&&o[0]===":"){o.shift();f.target=rt(o)}else if(a==="throttle"&&o[0]===":"){o.shift();f.throttle=d(O(o,E))}else if(a==="queue"&&o[0]===":"){o.shift();f.queue=O(o,E)}else if(a==="root"&&o[0]===":"){o.shift();f[a]=rt(o)}else if(a==="threshold"&&o[0]===":"){o.shift();f[a]=O(o,E)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}r.push(f)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=a(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){oe(e).cancelled=true}function ct(e,t,n){const r=oe(e);r.timeout=b().setTimeout(function(){if(se(e)&&r.cancelled!==true){if(!pt(n,e,Bt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function at(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){gt(t,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){if(e.type==="submit"&&t.tagName==="FORM"){return true}else if(e.type==="click"){const n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit"){return true}const r=t.closest("a");const o=/^#.+/;if(r&&r.href&&!o.test(r.getAttribute("href"))){return true}}return false}function dt(e,t){return oe(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(te().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function gt(l,c,e,u,f){const a=oe(l);let t;if(u.from){t=m(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in a)){a.lastValue=new WeakMap}t.forEach(function(e){if(!a.lastValue.has(u)){a.lastValue.set(u,new WeakMap)}a.lastValue.get(u).set(e,e.value)})}ie(t,function(i){const s=function(e){if(!se(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(f||ht(e,i)){e.preventDefault()}if(pt(u,l,e)){return}const t=oe(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ce(e.target),u.target)){return}}if(u.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(u.changed){const n=e.target;const r=n.value;const o=a.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(u.throttle>0){if(!a.throttle){ae(l,"htmx:trigger");c(l,e);a.throttle=b().setTimeout(function(){a.throttle=null},u.throttle)}}else if(u.delay>0){a.delayed=b().setTimeout(function(){ae(l,"htmx:trigger");c(l,e)},u.delay)}else{ae(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let yt=null;function xt(){if(!yt){yt=function(){mt=true};window.addEventListener("scroll",yt);window.addEventListener("resize",yt);setInterval(function(){if(mt){mt=false;ie(te().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&M(e)){e.setAttribute("data-hx-revealed","true");const t=oe(e);if(t.initHash){ae(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){ae(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;ae(e,"htmx:trigger");t(e)}};if(r>0){b().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;ie(de,function(r){if(s(t,"hx-"+r)){const o=a(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){xt();gt(r,n,t,e);bt(ce(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ue(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ce(r),n,e)}else{gt(r,n,t,e)}}function Et(e){const t=ce(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=At(e.target);const n=Nt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Nt(e);if(t){t.lastButtonClicked=null}}function At(e){return g(ce(e),"button, input[type='submit']")}function Lt(e){return e.form||g(e,"form")}function Nt(e){const t=At(e.target);if(!t){return}const n=Lt(t);if(!n){return}return oe(n)}function It(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Pt(t,e,n){const r=oe(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){On(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Dt(t){De(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Jt(t){if(!B()){return null}t=U(t);const n=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){r.response=this.response;ae(te().body,"htmx:historyCacheMissLoad",r);ze(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:true});$t(r.path);ae(te().body,"htmx:historyRestore",{path:e,cacheMiss:true,serverResponse:r.response})}else{fe(te().body,"htmx:historyCacheMissLoadError",r)}};if(ae(te().body,"htmx:historyCacheMiss",r)){t.send()}}function en(e){Gt();e=e||location.pathname+location.search;const t=Jt(e);if(t){const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll};const r={path:e,item:t,historyElt:_t(),swapSpec:n};if(ae(te().body,"htmx:historyCacheHit",r)){ze(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title});$t(r.path);ae(te().body,"htmx:historyRestore",r)}}else{if(Q.config.refreshOnHistoryMiss){Q.location.reload(true)}else{Qt(e)}}}function tn(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function nn(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function rn(e,t){ie(e.concat(t),function(e){const t=oe(e);t.requestCount=(t.requestCount||1)-1});ie(e,function(e){const t=oe(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});ie(t,function(e){const t=oe(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function on(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);ie(e,e=>r.append(t,e))}}function un(e){if(e instanceof HTMLSelectElement&&e.multiple){return F(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e instanceof HTMLInputElement&&e.files){return F(e.files)}return e.value}function fn(t,n,r,e,o){if(e==null||on(t,e)){return}else{t.push(e)}if(sn(e)){const i=ee(e,"name");ln(i,un(e),n);if(o){an(e,r)}}if(e instanceof HTMLFormElement){ie(e.elements,function(e){if(t.indexOf(e)>=0){cn(e.name,un(e),n)}else{t.push(e)}if(o){an(e,r)}});new FormData(e).forEach(function(e,t){if(e instanceof File&&e.name===""){return}ln(t,e,n)})}}function an(e,t){const n=e;if(n.willValidate){ae(n,"htmx:validation:validate");if(!n.checkValidity()){if(ae(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&Q.config.reportValidityOfForms){n.reportValidity()}t.push({elt:n,message:n.validationMessage,validity:n.validity})}}}function hn(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function dn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=oe(e);if(s.lastButtonClicked&&!se(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||a(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){fn(n,o,i,Lt(e),l)}fn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const f=ee(u,"name");ln(f,u.value,o)}const c=we(e,"hx-include");ie(c,function(e){fn(n,r,i,ce(e),l);if(!h(e,"form")){ie(p(e).querySelectorAll(ot),function(e){fn(n,r,i,e,l)})}});hn(r,o);return{errors:i,formData:r,values:kn(r)}}function pn(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function gn(e){e=Pn(e);let n="";e.forEach(function(e,t){n=pn(n,t,e)});return n}function mn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":a(t,"id"),"HX-Current-URL":location.href};Cn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(oe(e).boosted){r["HX-Boosted"]="true"}return r}function yn(n,e){const t=ne(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){ie(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;ie(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function xn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function bn(e,t){const n=t||ne(e,"hx-swap");const r={swapStyle:oe(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&oe(e).boosted&&!xn(e)){r.show="top"}if(n){const s=X(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const f=l.slice(5);var o=f.split(":");const a=o.pop();var i=o.length>0?o.join(":"):null;r.show=a;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{R("Unknown modifier in hx-swap: "+l)}}}}return r}function vn(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function wn(t,n,r){let o=null;Vt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(vn(n)){return hn(new FormData,Pn(r))}else{return gn(r)}}}function Sn(e){return{tasks:[],elts:[e]}}function En(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ce(ue(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}if(typeof t.scroll==="number"){b().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ce(ue(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Cn(r,e,o,i,s){if(i==null){i={}}if(r==null){return i}const l=a(r,e);if(l){let e=l.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=On(r,function(){if(s){return Function("event","return ("+e+")").call(r,s)}else{return Function("return ("+e+")").call(r)}},{})}else{n=v(e)}for(const c in n){if(n.hasOwnProperty(c)){if(i[c]==null){i[c]=n[c]}}}}return Cn(ce(u(r)),e,o,i,s)}function On(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Rn(e,t,n){return Cn(e,"hx-vars",true,n,t)}function Hn(e,t,n){return Cn(e,"hx-vals",false,n,t)}function Tn(e,t){return le(Rn(e,t),Hn(e,t))}function qn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function An(t){if(t.responseURL){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function H(e,t){return t.test(e.getAllResponseHeaders())}function Ln(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:w(r)||ve,returnPromise:true})}else{let e=w(r.target);if(r.target&&!e||r.source&&!e&&!w(r.source)){e=ve}return he(t,n,w(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Nn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function In(e,t,n){const r=new URL(t,location.protocol!=="about:"?location.href:window.origin);const o=location.protocol!=="about:"?location.origin:window.origin;const i=o===r.origin;if(Q.config.selfRequestsOnly){if(!i){return false}}return ae(e,"htmx:validateUrl",le({url:r,sameHost:i},n))}function Pn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Dn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function kn(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Dn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,k){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=te().body}const F=i.handler||Vn;const M=i.select||null;if(!se(r)){re(s);return e}const c=i.targetOverride||ce(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:ne(r,"hx-target")});re(l);return e}let u=oe(r);const f=u.lastButtonClicked;if(f){const A=ee(f,"formaction");if(A!=null){n=A}const L=ee(f,"formmethod");if(L!=null){if(de.includes(L.toLowerCase())){t=L}else{re(s);return e}}}const a=ne(r,"hx-confirm");if(k===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:a};if(ae(r,"htmx:confirm",G)===false){re(s);return e}}let h=r;let d=ne(r,"hx-sync");let p=null;let X=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ce(ue(r,I))}d=(N[1]||"drop").trim();u=oe(h);if(d==="drop"&&u.xhr&&u.abortable!==true){re(s);return e}else if(d==="abort"){if(u.xhr){re(s);return e}else{X=true}}else if(d==="replace"){ae(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");p=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){ae(h,"htmx:abort")}else{if(p==null){if(o){const P=oe(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){p=P.triggerSpec.queue}}if(p==null){p="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(p==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}re(s);return e}}const g=new XMLHttpRequest;u.xhr=g;u.abortable=X;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=ne(r,"hx-prompt");if(B){var y=prompt(B);if(y===null||!ae(r,"htmx:prompt",{prompt:y,target:c})){re(s);m();return e}}if(a&&!k){if(!confirm(a)){re(s);m();return e}}let x=mn(r,c,y);if(t!=="get"&&!vn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=le(x,i.headers)}const U=dn(r,t);let b=U.errors;const V=U.formData;if(i.values){hn(V,Pn(i.values))}const j=Pn(Tn(r,o));const v=hn(V,j);let w=yn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=location.href}const S=Cn(r,"hx-request");const $=oe(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:$,useUrlParams:E,formData:w,parameters:kn(w),unfilteredFormData:v,unfilteredParameters:kn(v),headers:x,elt:r,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!ae(r,"htmx:configRequest",C)){re(s);m();return e}n=C.path;t=C.verb;x=C.headers;w=Pn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){ae(r,"htmx:validation:halted",C);re(s);m();return e}const _=n.split("#");const z=_[0];const O=_[1];let R=n;if(E){R=z;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=gn(w);if(O){R+="#"+O}}}if(!In(r,R,C)){fe(r,"htmx:invalidPath",C);re(l);m();return e}g.open(t.toUpperCase(),R,true);g.overrideMimeType("text/html");g.withCredentials=C.withCredentials;g.timeout=C.timeout;if(S.noHeaders){}else{for(const D in x){if(x.hasOwnProperty(D)){const Y=x[D];qn(g,D,Y)}}}const H={xhr:g,target:c,requestConfig:C,etc:i,boosted:$,select:M,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};g.onload=function(){try{const t=Nn(r);H.pathInfo.responsePath=An(g);F(r,H);if(H.keepIndicators!==true){rn(T,q)}ae(r,"htmx:afterRequest",H);ae(r,"htmx:afterOnLoad",H);if(!se(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(se(n)){e=n}}if(e){ae(e,"htmx:afterRequest",H);ae(e,"htmx:afterOnLoad",H)}}re(s)}catch(e){fe(r,"htmx:onLoadError",le({error:e},H));throw e}finally{m()}};g.onerror=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);re(l);m()};g.onabort=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);re(l);m()};g.ontimeout=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);re(l);m()};if(!ae(r,"htmx:beforeRequest",H)){re(s);m();return e}var T=tn(r);var q=nn(r);ie(["loadstart","loadend","progress","abort"],function(t){ie([g,g.upload],function(e){e.addEventListener(t,function(e){ae(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ae(r,"htmx:beforeSend",H);const J=E?null:wn(g,r,w);g.send(J);return e}function Fn(e,t){const n=t.xhr;let r=null;let o=null;if(H(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(H(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(H(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=ne(e,"hx-push-url");const c=ne(e,"hx-replace-url");const u=oe(e).boosted;let f=null;let a=null;if(l){f="push";a=l}else if(c){f="replace";a=c}else if(u){f="push";a=s||i}if(a){if(a==="false"){return{}}if(a==="true"){a=s||i}if(t.pathInfo.anchor&&a.indexOf("#")===-1){a=a+"#"+t.pathInfo.anchor}return{type:f,path:a}}else{return{}}}function Mn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Xn(e){for(var t=0;t`+`.${t}{opacity:0;visibility: hidden} `+`.${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`+"")}}function Zn(){const e=te().querySelector('meta[name="htmx-config"]');if(e){return v(e.content)}else{return null}}function Yn(){const e=Zn();if(e){Q.config=le(Q.config,e)}}Gn(function(){Yn();Wn();let e=te().body;Mt(e);const t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=oe(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){en();ie(t,function(e){ae(e,"htmx:restored",{document:te(),triggerEvent:ae})})}else{if(n){n(e)}}};b().setTimeout(function(){ae(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/icon.png b/Tesses.YouTubeDownloader.Server/res/icon.png new file mode 100644 index 0000000..10c5aac Binary files /dev/null and b/Tesses.YouTubeDownloader.Server/res/icon.png differ diff --git a/Tesses.YouTubeDownloader.Server/res/loading-indicator.svg b/Tesses.YouTubeDownloader.Server/res/loading-indicator.svg new file mode 100644 index 0000000..770be66 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/loading-indicator.svg @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/material-symbols-outlined.woff2 b/Tesses.YouTubeDownloader.Server/res/material-symbols-outlined.woff2 new file mode 100644 index 0000000..524ab5d Binary files /dev/null and b/Tesses.YouTubeDownloader.Server/res/material-symbols-outlined.woff2 differ diff --git a/Tesses.YouTubeDownloader.Server/res/material-symbols-rounded.woff2 b/Tesses.YouTubeDownloader.Server/res/material-symbols-rounded.woff2 new file mode 100644 index 0000000..e36c8d4 Binary files /dev/null and b/Tesses.YouTubeDownloader.Server/res/material-symbols-rounded.woff2 differ diff --git a/Tesses.YouTubeDownloader.Server/res/material-symbols-sharp.woff2 b/Tesses.YouTubeDownloader.Server/res/material-symbols-sharp.woff2 new file mode 100644 index 0000000..7a9aa4a Binary files /dev/null and b/Tesses.YouTubeDownloader.Server/res/material-symbols-sharp.woff2 differ diff --git a/Tesses.YouTubeDownloader.Server/res/material-symbols-subset.woff2 b/Tesses.YouTubeDownloader.Server/res/material-symbols-subset.woff2 new file mode 100644 index 0000000..c4e06cd Binary files /dev/null and b/Tesses.YouTubeDownloader.Server/res/material-symbols-subset.woff2 differ diff --git a/Tesses.YouTubeDownloader.Server/res/theme.css b/Tesses.YouTubeDownloader.Server/res/theme.css new file mode 100644 index 0000000..d35475f --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/theme.css @@ -0,0 +1,78 @@ +:root, +body.light { +--primary:#c00100; +--on-primary:#ffffff; +--primary-container:#ffdad4; +--on-primary-container:#410000; +--secondary:#775651; +--on-secondary:#ffffff; +--secondary-container:#ffdad4; +--on-secondary-container:#2c1512; +--tertiary:#705c2e; +--on-tertiary:#ffffff; +--tertiary-container:#fbdfa6; +--on-tertiary-container:#251a00; +--error:#ba1a1a; +--on-error:#ffffff; +--error-container:#ffdad6; +--on-error-container:#410002; +--background:#fffbff; +--on-background:#201a19; +--surface:#fff8f6; +--on-surface:#201a19; +--surface-variant:#f5ddda; +--on-surface-variant:#534341; +--outline:#857370; +--outline-variant:#d8c2be; +--shadow:#000000; +--scrim:#000000; +--inverse-surface:#362f2e; +--inverse-on-surface:#fbeeec; +--inverse-primary:#ffb4a8; +--surface-dim:#e4d7d5; +--surface-bright:#fff8f6; +--surface-container-lowest:#ffffff; +--surface-container-low:#fef1ee; +--surface-container:#f8ebe9; +--surface-container-high:#f3e5e3; +--surface-container-highest:#ede0dd; +} + +body.dark { + --primary:#ffb4a8; +--on-primary:#690100; +--primary-container:#930100; +--on-primary-container:#ffdad4; +--secondary:#e7bdb6; +--on-secondary:#442925; +--secondary-container:#5d3f3b; +--on-secondary-container:#ffdad4; +--tertiary:#dec48c; +--on-tertiary:#3e2e04; +--tertiary-container:#564419; +--on-tertiary-container:#fbdfa6; +--error:#ffb4ab; +--on-error:#690005; +--error-container:#93000a; +--on-error-container:#ffb4ab; +--background:#201a19; +--on-background:#ede0dd; +--surface:#181211; +--on-surface:#ede0dd; +--surface-variant:#534341; +--on-surface-variant:#d8c2be; +--outline:#a08c89; +--outline-variant:#534341; +--shadow:#000000; +--scrim:#000000; +--inverse-surface:#ede0dd; +--inverse-on-surface:#362f2e; +--inverse-primary:#c00100; +--surface-dim:#181211; +--surface-bright:#3f3736; +--surface-container-lowest:#120d0c; +--surface-container-low:#201a19; +--surface-container:#251e1d; +--surface-container-high:#2f2827; +--surface-container-highest:#3b3332; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/tytd.svg b/Tesses.YouTubeDownloader.Server/res/tytd.svg new file mode 100644 index 0000000..db7c33f --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/tytd.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + diff --git a/Tesses.YouTubeDownloader.Server/src/components/add-to-personal-list.tcross b/Tesses.YouTubeDownloader.Server/src/components/add-to-personal-list.tcross new file mode 100644 index 0000000..1224096 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/add-to-personal-list.tcross @@ -0,0 +1,38 @@ +func Components.AddToPersonalList(tytd,id) +{ + + return
+ +
+
+
+ + + arrow_drop_down +
+
+ + +
+
; +} + +func Components.CreateNewList(tytd,id) +{ + return
+ +
+
+ + +
+ + +
+
; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/add-video.tcross b/Tesses.YouTubeDownloader.Server/src/components/add-video.tcross new file mode 100644 index 0000000..057dbd7 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/add-video.tcross @@ -0,0 +1,30 @@ +func Components.AddVideo(id) +{ + return
+ +
+
+
+ + + arrow_drop_down +
+
+ + + + +
+
; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/add.tcross b/Tesses.YouTubeDownloader.Server/src/components/add.tcross new file mode 100644 index 0000000..122f9bb --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/add.tcross @@ -0,0 +1,33 @@ +func Components.Add() +{ + return
+
+ +
+
+
+
+ + + arrow_drop_down +
+
+ + + + + +
+
; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/discover_entry.tcross b/Tesses.YouTubeDownloader.Server/src/components/discover_entry.tcross new file mode 100644 index 0000000..5013e62 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/discover_entry.tcross @@ -0,0 +1,62 @@ +func Components.DiscoverEntry(item) +{ + switch(item.type) + { + case "video": + return +
+
+ +
+
+
+ +
+ {item.views} • {item.uploaded} +
+
+ + + + {part.text} + + + + {part.text} + + + +
+ +
+
+
; + break; + case "channel": + return +
+
+ +
+
+
+ +
+ {item.subs} +
+
+

{item.description}

+
+
+ +
+
; + break; + } + + return ""; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/downloaded-entry.tcross b/Tesses.YouTubeDownloader.Server/src/components/downloaded-entry.tcross new file mode 100644 index 0000000..4649a7c --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/downloaded-entry.tcross @@ -0,0 +1,60 @@ +func Components.DownloadedVideo(item) +{ + return +
+
+ +
+
+
+ +
+ {item.viewCountStr} (when downloaded) +
+
+ {item.author} + + +
+ +
+
+
; + + +} +func Components.DownloadedPlaylist(item) +{ + var res =
+
+ +
+
+ +
+
; + return res; + +} +func Components.DownloadedChannel(item) +{ + var res =
+
+ +
+ +
; + return res; + +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/music-album-entry.tcross b/Tesses.YouTubeDownloader.Server/src/components/music-album-entry.tcross new file mode 100644 index 0000000..58c779c --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/music-album-entry.tcross @@ -0,0 +1,11 @@ +func Components.MusicAlbum(item) +{ + return
+
+ +
+ +
; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/music-artist.tcross b/Tesses.YouTubeDownloader.Server/src/components/music-artist.tcross new file mode 100644 index 0000000..d62e640 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/music-artist.tcross @@ -0,0 +1,4 @@ +func Components.MusicArtist(item) +{ + return {item.name}; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/package-dl-item.tcross b/Tesses.YouTubeDownloader.Server/src/components/package-dl-item.tcross new file mode 100644 index 0000000..8ef1854 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/package-dl-item.tcross @@ -0,0 +1,77 @@ +func Components.PackageItem(tytd,item) +{ + + var html =
+
+ Package thumbnail +
+
+
+ +
+ {item.version} +
+
+
+
+ +
+
; + + return html; +} +func Components.InstallButton(tytd, name, version, $confirm) +{ + var id = Crypto.Base64Encode(new ByteArray($"{name}-{version}")).Replace("=",""); + + var version = Version.Parse(version); + + if(version != null) + { + + var state = tytd.PackageState(name,version); + return
+ + + + + + + +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + +
+ +
; + } + return ""; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/plugin.tcross b/Tesses.YouTubeDownloader.Server/src/components/plugin.tcross new file mode 100644 index 0000000..dda889c --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/plugin.tcross @@ -0,0 +1,30 @@ +func Components.InstalledPlugin(item) +{ + + + var html =
+
+ Package thumbnail +
+
+
+
+ + + {TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)} + + + {TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)} + + +
+
+ {item.version} +
+
+ +
+
; + + return html; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/progress.tcross b/Tesses.YouTubeDownloader.Server/src/components/progress.tcross new file mode 100644 index 0000000..660eeae --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/progress.tcross @@ -0,0 +1,13 @@ +var progress=0; +func Components.Progress(tytd) +{ + var vid = tytd.CurrentVideo; + + var html =
+

{vid.Title}

+

{vid.Channel}

+ +
; + progress++; + return html; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/shell.tcross b/Tesses.YouTubeDownloader.Server/src/components/shell.tcross new file mode 100644 index 0000000..73a40ad --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/shell.tcross @@ -0,0 +1,68 @@ +func Components.Shell(title, html, page, $mypage) +{ + if(TypeOf(mypage) != "Path") + mypage = /; + + var index = (/).MakeRelative(mypage).ToString(); + if(index == "") index = "./"; + + var pages = [ + { + text="Home", + icon="home", + href=index, + classStr="" + }, + { + text = "Downloads", + icon = "download", + href=(/"downloads").MakeRelative(mypage).ToString(), + classStr="" + }, + { + text = "Plugins", + icon = "extension", + href= (/"plugins").MakeRelative(mypage).ToString(), + classStr="" + }, + { + text = "Settings", + icon = "settings", + href= (/"settings").MakeRelative(mypage).ToString(), + classStr="" + } + ]; + pages[page].classStr = "active"; + return + + + + + TYTD - {title} + + + + + + + + + +
+
+ +
+ +
+ + + +; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/subscribebtn.tcross b/Tesses.YouTubeDownloader.Server/src/components/subscribebtn.tcross new file mode 100644 index 0000000..b92d8d7 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/subscribebtn.tcross @@ -0,0 +1,46 @@ +func Components.Subscribe(tytd,id) +{ + var bell = tytd.GetSubscriptionBell(id); + var subscribed= bell != null; + + var bellLogo = "notifications_off"; + switch(bell) + { + case SubscriptionBell.DownloadLow: + case SubscriptionBell.DownloadHigh: + bellLogo = "download"; + break; + case SubscriptionBell.Bell: + case SubscriptionBell.BellLow: + case SubscriptionBell.BellHigh: + bellLogo="notifications_active"; + break; + } + + +
+ + +
+
+ +
+
+ +
+
+
+ + + + +
+
+} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/main.tcross b/Tesses.YouTubeDownloader.Server/src/main.tcross new file mode 100644 index 0000000..02e4868 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/main.tcross @@ -0,0 +1,374 @@ +var TYTDResources = [ + {path="/beer.min.css",value=embed("beer.min.css")}, + {path="/beer.min.js",value=embed("beer.min.js")}, + {path="/material-symbols-outlined.woff2",value=embed("material-symbols-outlined.woff2")}, + {path="/material-symbols-rounded.woff2",value=embed("material-symbols-rounded.woff2")}, + {path="/material-symbols-sharp.woff2",value=embed("material-symbols-sharp.woff2")}, + {path="/material-symbols-subset.woff2",value=embed("material-symbols-subset.woff2")}, + {path="/htmx.min.js",value=embed("htmx.min.js")}, + {path="/favicon.ico",value=embed("favicon.ico")}, + {path="/tytd.svg",value=embed("tytd.svg")}, + {path="/loading-indicator.svg",value=embed("loading-indicator.svg")}, + {path="/theme.css",value=embed("theme.css")} +]; +var times=1; + +class TYTDApp { + private TYTD; + + public TYTDApp() + { + + var tytdfs = new SubdirFilesystem(FS.Local, "TYTD"); + this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull("TYTD")); + this.TYTD.Start(); + + } + public Handle(ctx) + { + if(ctx.Path == "/") + { + ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD)); + return true; + } + else if(ctx.Path == "/api/v1/download") + { + var v = ctx.QueryParams.TryGetFirst("v"); + var inline = ctx.QueryParams.GetFirstBoolean("inline"); + var res = ctx.QueryParams.TryGetFirst("res"); + + if(TypeOf(res) != "String") res = Resolution.LowVideo; + + var path = this.TYTD.GetVideoPath(v,res); + + if(path != null) + { + var info = this.TYTD.GetVideo(v); + var filename = $"{info.title}-{info.videoId}.{path.GetExtension()}"; + var strm = this.TYTD.Storage.OpenFile(path,"rb"); + ctx.WithMimeType(Net.Http.MimeType(path.ToString())).WithContentDisposition(filename,inline).SendStream(strm); + strm.Close(); + } + } + else if(ctx.Path == "/api/v1/playlist.json") + { + var id = ctx.QueryParams.TryGetFirst("id"); + var page = ctx.QueryParams.TryGetFirstInt("page"); + var count = ctx.QueryParams.TryGetFirstInt("count"); + if(TypeOf(page)!="Long") page = 1; + if(TypeOf(count)!="Long") count = 20; + if(TypeOf(id)!="String") id = ""; + ctx.WithMimeType("application/json").SendJson(this.TYTD.GetPlaylistContents(id,page-1,count)); + return true; + } + else if(ctx.Path == "/api/v1/channel.json") + { + var id = ctx.QueryParams.TryGetFirst("id"); + var page = ctx.QueryParams.TryGetFirstInt("page"); + var count = ctx.QueryParams.TryGetFirstInt("count"); + if(TypeOf(page)!="Long") page = 1; + if(TypeOf(count)!="Long") count = 20; + if(TypeOf(id)!="String") id = ""; + ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count)); + return true; + } + else if(ctx.Path == "/api/v1/downloads.json") + { + var q = ctx.QueryParams.TryGetFirst("q"); + var type = ctx.QueryParams.TryGetFirst("type"); + var page = ctx.QueryParams.TryGetFirstInt("page"); + var count = ctx.QueryParams.TryGetFirstInt("count"); + if(TypeOf(page)!="Long") page = 1; + if(TypeOf(count)!="Long") count = 20; + if(TypeOf(type)!="String") type = "videos"; + if(TypeOf(q)!="String") q = ""; + + var result = { entries = [] }; + switch(type) + { + case "videos": + + result.entries = this.TYTD.GetVideos(q,page-1,count); + + break; + case "playlists": + result.entries = this.TYTD.GetPlaylists(q,page-1,count); + break; + case "channels": + result.entries = this.TYTD.GetChannels(q,page-1,count); + break; + + } + ctx.WithMimeType("application/json").SendJson(result); + return true; + } + else if(ctx.Path == "/subscribe") + { + var id = ctx.QueryParams.TryGetFirst("id"); + var bell = ctx.QueryParams.TryGetFirst("bell"); + var type = ctx.QueryParams.TryGetFirst("type"); + + + switch(type) + { + case "subscribe": + this.TYTD.SetSubscriptionBell(id, SubscriptionBell.BellLow); + + break; + case "unsubscribe": + this.TYTD.SetSubscriptionBell(id, null); + break; + case "bell": + this.TYTD.SetSubscriptionBell(id, bell); + break; + } + ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id)); + return true; + } + else if(ctx.Path == "/downloads") + { + ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/list") + { + ctx.WithMimeType("text/html").SendText(Pages.List(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/settings") + { + if(ctx.Method == "POST") + { + /* + hours + minutes + seconds + 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"); + + if(TypeOf(tag) == "String" && TypeOf(hours) == "Long" && TypeOf(minutes) == "Long" && TypeOf(seconds) == "Long") + { + seconds += minutes * 60; + seconds += hours * 3600; + this.TYTD.Config.TYTDTag = tag; + this.TYTD.Config.BellTimer = seconds; + this.TYTD.Config.EnablePlugins = enablePlugins; + this.TYTD.SaveConfig(); + } + } + ctx.WithMimeType("text/html").SendText(Pages.Settings(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/video") + { + ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/playlist") + { + ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/channel") + { + ctx.WithMimeType("text/html").SendText(Pages.ChannelInfo(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/add") + { + var url = ctx.QueryParams.TryGetFirst("url"); + var action =ctx.QueryParams.TryGetFirst("action"); + var res = ctx.QueryParams.TryGetFirst("res"); + if(action == "add") + { + this.TYTD.DownloadItem(url,res); + } + else if(action == "info") + { + + ctx.ResponseHeaders.SetValue("HX-Redirect", TYTD.PageRedirect(url)); + + + + } + else if(action == "download") + { + ctx.ResponseHeaders.SetValue("HX-Redirect", $"./api/v1/download?v={Net.Http.UrlEncode(url)}&res={Net.Http.UrlEncode(res)}"); + + } + else if(action == "play") + { + ctx.ResponseHeaders.SetValue("HX-Redirect", $"./api/v1/download?v={Net.Http.UrlEncode(url)}&res={Net.Http.UrlEncode(res)}&inline=true"); + + } + ctx.WithMimeType("text/html").SendText(Components.Add()); + return true; + } + else if(ctx.Path == "/add-video") + { + var url = ctx.QueryParams.TryGetFirst("url"); + var action =ctx.QueryParams.TryGetFirst("action"); + var res = ctx.QueryParams.TryGetFirst("res"); + if(action == "add") + { + this.TYTD.DownloadItem(url,res); + } + + else if(action == "download") + { + ctx.ResponseHeaders.SetValue("HX-Redirect", $"./api/v1/download?v={Net.Http.UrlEncode(url)}&res={Net.Http.UrlEncode(res)}"); + + } + else if(action == "play") + { + ctx.ResponseHeaders.SetValue("HX-Redirect", $"./api/v1/download?v={Net.Http.UrlEncode(url)}&res={Net.Http.UrlEncode(res)}&inline=true"); + + } + ctx.WithMimeType("text/html").SendText(Components.AddVideo(url)); + return true; + } + else if(ctx.Path == "/add-to-list") + { + var id = ctx.QueryParams.TryGetFirst("id"); + if(TypeOf(id) != "String") id = ""; + + var name = ctx.QueryParams.TryGetFirst("name"); + if(TypeOf(name) != "String") name = ""; + + if(name == "") + { + ctx.WithMimeType("text/html").SendText(Components.CreateNewList(this.TYTD,id)); + } + else { + this.TYTD.AddToPersonalList(name,id); + ctx.WithMimeType("text/html").SendText(Components.AddToPersonalList(this.TYTD,id)); + } + + + return true; + } + else if(ctx.Path == "/progress") + { + ctx.WithMimeType("text/html").SendText(Components.Progress(this.TYTD)); + return true; + } + else if(ctx.Path == "/plugins") + { + ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/plugins-download") + { + ctx.WithMimeType("text/html").SendText(Pages.DownloadPlugins(this.TYTD,ctx)); + return true; + } + else if(ctx.Path == "/package-manage" && ctx.Method == "POST") + { + var data = { + name = ctx.QueryParams.TryGetFirst("name"), + version = ctx.QueryParams.TryGetFirst("version"), + action = ctx.QueryParams.TryGetFirst("action") + }; + + var mustConfirm = false; + + + switch(data.action) + { + case "install": + { + var v = Version.Parse(data.version); + if(v != null) + this.TYTD.PackageInstall(data.name,v); + } + break; + case "uninstall": + { + mustConfirm = true; + } + break; + case "confirm": + { + this.TYTD.PackageUninstall(data.name); + } + break; + } + ctx.WithMimeType("text/html").SendText(Components.InstallButton(this.TYTD, data.name, data.version, mustConfirm)); + return true; + } + else if(ctx.Path == "/api/v1/video-thumbnail") + { + var v = ctx.QueryParams.TryGetFirst("v"); + var res = ctx.QueryParams.TryGetFirst("res"); + if(TypeOf(res) != "String") res = "0"; + var thu = this.TYTD.GetVideoThumbnail(v,res); + if(thu == null) return false; + ctx.WithMimeType("image/jpg").SendBytes(thu); + return true; + } + else if(ctx.Path == "/api/v1/playlist-thumbnail") + { + var id = ctx.QueryParams.TryGetFirst("id"); + var res = ctx.QueryParams.TryGetFirst("res"); + if(TypeOf(res) != "String") res = "0"; + var thu = this.TYTD.GetPlaylistThumbnail(id,res); + if(thu == null) return false; + ctx.WithMimeType("image/jpg").SendBytes(thu); + return true; + } + else if(ctx.Path == "/api/v1/channel-thumbnail") + { + var id = ctx.QueryParams.TryGetFirst("id"); + var res = ctx.QueryParams.TryGetFirst("res"); + if(TypeOf(res) != "String") res = "0"; + var thu = this.TYTD.GetChannelThumbnail(id,res); + if(thu == null) return false; + ctx.WithMimeType("image/jpg").SendBytes(thu); + return true; + } + else if(ctx.Path == "/api/v1/plugin-thumbnail.png") + { + var name = ctx.QueryParams.TryGetFirst("name"); + ctx.WithMimeType().SendBytes(this.TYTD.GetPluginThumbnail(name)); + return true; + } + else if(ctx.Path.StartsWith("/plugin/")) + { + return this.TYTD.Servers.Handle(ctx); + } + else { + each(var file : TYTDResources) + { + if(ctx.Path == file.path) + { + ctx.WithMimeType(Net.Http.MimeType(file.path)).SendBytes(file.value); + return true; + } + } + } + return false; + } + + public Close() + { + this.TYTD.Stop(); + } +} + +func WebAppMain(args) +{ + return new TYTDApp(); +} +func main(args) +{ + var res = WebAppMain(args); + Net.Http.ListenSimpleWithLoop(res,3255); + res.Close(); + return 0; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/channel/info.tcross b/Tesses.YouTubeDownloader.Server/src/pages/channel/info.tcross new file mode 100644 index 0000000..12ec196 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/channel/info.tcross @@ -0,0 +1,39 @@ +func Pages.ChannelInfo(tytd,ctx) +{ + var id = ctx.QueryParams.TryGetFirst("id"); + var page = ctx.QueryParams.TryGetFirstInt("page"); + if(TypeOf(page) != "Long") page = 1; + page--; + + var html =

Could not find channel

; + var title = "N/A"; + + if(TypeOf(id) == "String") + { + id = TYTD.GetChannelId(id); + var res = tytd.GetChannelContents(id,page,20); + + html = +
+
+

{res.authorName}

+
+
+ +
+
+ + + +
+ + {page+1} + +
+
; + + title = res.authorName; + } + + return Components.Shell($"Channel {title}",html,1); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/discover.tcross b/Tesses.YouTubeDownloader.Server/src/pages/discover.tcross new file mode 100644 index 0000000..c05faf2 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/discover.tcross @@ -0,0 +1,34 @@ +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 = +
+
+ search + + +
+
+ + +
    + +
  • + +
+ +
; + + + return Components.Shell($"Discover {q2}",html,1); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/downloads.tcross b/Tesses.YouTubeDownloader.Server/src/pages/downloads.tcross new file mode 100644 index 0000000..15cd72c --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/downloads.tcross @@ -0,0 +1,293 @@ +func Pages.Downloads(tytd,ctx) +{ + var type = ctx.QueryParams.TryGetFirst("type"); + var q = ctx.QueryParams.TryGetFirst("q"); + var page = ctx.QueryParams.TryGetFirstInt("page"); + if(TypeOf(page) != "Long") page = 1; + page--; + + if(TypeOf(q) != "String") + { + q = ""; + } + if(TypeOf(type) != "String") + { + type = "videos"; + } + var html =""; + + switch(type) { + case "videos": + { + + var res = tytd.GetVideos(q,page,20); + html = +
+
+
+
+ + + arrow_drop_down +
+
+
+ +
+
+
+ +
+
+ search + + + +
+
+ + + + +
+ + {page+1} + +
+ +
; + } + break; + case "playlists": + { + + var res = tytd.GetPlaylists(q,page,20); + html = +
+
+
+
+ + + arrow_drop_down +
+
+
+ +
+
+
+ +
+
+ search + + + +
+
+ + + + +
+ + {page+1} + +
+ +
; + } + break; + case "channels": + { + + var res = tytd.GetChannels(q,page,20); + html = +
+
+
+
+ + + arrow_drop_down +
+
+
+ +
+
+
+ +
+
+ search + + + +
+
+ + + + +
+ + {page+1} + +
+ +
; + } + break; + case "personal": + { + var res = tytd.GetPersonalLists(); + html = +
+
+
+
+ + + arrow_drop_down +
+
+
+ +
+
+
+ + + + + + +
+
+ +
+ + +
+
+ + + + + + + +
; + } + + + break; + } + + + + + return Components.Shell($"Downloads {q}",html,1); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/index.tcross b/Tesses.YouTubeDownloader.Server/src/pages/index.tcross new file mode 100644 index 0000000..0bcc1a2 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/index.tcross @@ -0,0 +1,11 @@ +func Pages.Index(tytd) +{ + + var html = + + + ; + + + return Components.Shell("Home",html,0); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/list.tcross b/Tesses.YouTubeDownloader.Server/src/pages/list.tcross new file mode 100644 index 0000000..a7cd1b2 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/list.tcross @@ -0,0 +1,36 @@ +func Pages.List(tytd,ctx) +{ + var name = ctx.QueryParams.TryGetFirst("name"); + var page = ctx.QueryParams.TryGetFirstInt("page"); + if(TypeOf(page) != "Long") page = 1; + page--; + if(TypeOf(name) != "String") + { + name = ""; + } + + var res = tytd.GetPersonalListContents(name, page, 20); + + var html = +
+
+ +
+
+

{name}

+
+
+ + + +
+ + {page+1} + +
+ +
; + + + return Components.Shell($"Personal List {name}",html,1); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/music/artist.tcross b/Tesses.YouTubeDownloader.Server/src/pages/music/artist.tcross new file mode 100644 index 0000000..2107ed6 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/music/artist.tcross @@ -0,0 +1,25 @@ +func Pages.Artist(music,ctx) +{ + + var id = ctx.QueryParams.TryGetFirst("id"); + var res = music.GetArtistsAlbums(id); + + if(TypeOf(id) != "String") + { + return Components.Shell("Error",

Artist id not specified

,pages); + } + + + var html = +

{res.name}

+
    + +
  • + +
+
; + + + return Components.Shell($"Artist {res.name}",html,2); + +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/music/artists.tcross b/Tesses.YouTubeDownloader.Server/src/pages/music/artists.tcross new file mode 100644 index 0000000..100fadf --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/music/artists.tcross @@ -0,0 +1,33 @@ +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 = +
+
+ search + + +
+
+ + +
    + +
  • + +
+
; + + + return Components.Shell($"Search Artists {q2}",html,2); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/music/music.tcross b/Tesses.YouTubeDownloader.Server/src/pages/music/music.tcross new file mode 100644 index 0000000..4da2516 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/music/music.tcross @@ -0,0 +1,8 @@ +func Pages.Music(ctx) +{ + var html = ; + return Components.Shell("Music",html,2); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/playlist/info.tcross b/Tesses.YouTubeDownloader.Server/src/pages/playlist/info.tcross new file mode 100644 index 0000000..773783c --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/playlist/info.tcross @@ -0,0 +1,39 @@ +func Pages.PlaylistInfo(tytd,ctx) +{ + var id = ctx.QueryParams.TryGetFirst("id"); + var page = ctx.QueryParams.TryGetFirstInt("page"); + if(TypeOf(page) != "Long") page = 1; + page--; + + var html =

Could not find playlist

; + var title = "N/A"; + + if(TypeOf(id) == "String") + { + id = TYTD.GetPlaylistId(id); + var res = tytd.GetPlaylistContents(id,page,20); + + html = +
+
+

{res.title}

+
+ +
+ + + +
+ + {page+1} + +
+
; + + title = res.authorName; + } + + return Components.Shell($"Playlist {title}",html,1); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/plugins-dl.tcross b/Tesses.YouTubeDownloader.Server/src/pages/plugins-dl.tcross new file mode 100644 index 0000000..5c03a2f --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/plugins-dl.tcross @@ -0,0 +1,79 @@ +func Pages.DownloadPlugins(tytd,ctx) +{ + var q = ctx.QueryParams.TryGetFirst("q"); + if(TypeOf(q) != "String") + { + q = ""; + } + var server = ctx.QueryParams.TryGetFirst("server"); + if(TypeOf(server) != "String") server = "https://cpkg.tesseslanguage.com/"; + var items=[]; + var items2 = []; + + each(var item : tytd.PackageManager.Search(q,{server,type="lib",pluginHost="tytd2025"})) + { + items2.Add({ + name = item.packageName, + version = item.version, + url = $"{server}/package?name={Net.Http.UrlEncode(item.packageName)}", + thumb = $"{server}/api/v1/package_icon.png?name={Net.Http.UrlEncode(item.packageName)}&version={Net.Http.UrlEncode(item.version)}" + }); + } + + + each(var item : tytd.PackageManager.GetPackageServers()) + { + items.Add({ + active = items.Count == 0, + url = item + }); + } + + + + + var html= +
+ +
+ + arrow_drop_down +
+
+
+ +
+ + +
+ +
+ + + + + +
; + + return Components.Shell("Download plugins",html ,2); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/plugins.tcross b/Tesses.YouTubeDownloader.Server/src/pages/plugins.tcross new file mode 100644 index 0000000..aaf63d8 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/plugins.tcross @@ -0,0 +1,21 @@ +func Pages.Plugins(tytd,ctx) +{ + var html= + + + + + + ; + + return Components.Shell("Installed plugins",html ,2); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/settings.tcross b/Tesses.YouTubeDownloader.Server/src/pages/settings.tcross new file mode 100644 index 0000000..ab0be33 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/settings.tcross @@ -0,0 +1,66 @@ +func Pages.Settings(tytd,ctx) +{ + var totalSecs = tytd.Config.BellTimer ?? 10800; + var enablePlugins = tytd.Config.EnablePlugins ?? true; + var hours = totalSecs / 3600; + totalSecs -= hours * 3600; + var minutes = totalSecs / 60; + var seconds = totalSecs % 60; + + + var html =
+
+ +
+
+ + +
+ +
+ Subscriber Poll Rate +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+ +
+
; + + return Components.Shell("Settings",html ,3); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/video/info.tcross b/Tesses.YouTubeDownloader.Server/src/pages/video/info.tcross new file mode 100644 index 0000000..d367cc4 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/video/info.tcross @@ -0,0 +1,30 @@ +func Pages.VideoInfo(tytd,ctx) +{ + var vid = ctx.QueryParams.TryGetFirst("v"); + tytd.PutVideoInfoIfNotExists(vid); + var vi = tytd.GetVideo(vid); + var html =

Could not find video

; + + if(vi != null) + { + html =
+
+ +
+
+ +
+
+ +
+
+

{vi.title}

+ {vi.author} +

{vi.shortDescription}

+
+ +
; + } + + return Components.Shell(vi != null ? vi.title : "Could not find video", html, 1); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/cross.json b/Tesses.YouTubeDownloader/cross.json new file mode 100644 index 0000000..03406a9 --- /dev/null +++ b/Tesses.YouTubeDownloader/cross.json @@ -0,0 +1,19 @@ +{ + "info": { + "description": "Download YouTube Videos (using CrossLang)", + "maintainer": "Mike Nolan", + "repo":"https://onedev.site.tesses.net/tytd2025", + "homepage": "https://tesses.net/apps/tytd/2025/", + "type": "lib", + "license": "GPLv3" + }, + "dependencies": [ + { + "name": "Tesses.CrossLang.BuildEssentials", + "version": "1.0.0.0-prod" + } + ], + "name": "Tesses.YouTubeDownloader", + "version": "1.0.0.0-prod", + "icon": "icon.png" +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/res/icon.png b/Tesses.YouTubeDownloader/res/icon.png new file mode 100644 index 0000000..10c5aac Binary files /dev/null and b/Tesses.YouTubeDownloader/res/icon.png differ diff --git a/Tesses.YouTubeDownloader/res/package_icon.png b/Tesses.YouTubeDownloader/res/package_icon.png new file mode 100644 index 0000000..e3805eb Binary files /dev/null and b/Tesses.YouTubeDownloader/res/package_icon.png differ diff --git a/Tesses.YouTubeDownloader/res/request.json b/Tesses.YouTubeDownloader/res/request.json new file mode 100644 index 0000000..33ebbf7 --- /dev/null +++ b/Tesses.YouTubeDownloader/res/request.json @@ -0,0 +1,23 @@ +{ + "context": { + "client": { + "clientName": "ANDROID", + "clientVersion": "19.28.35", + "clientScreen": "WATCH", + "platform": "MOBILE", + "osName": "Android", + "osVersion": "15", + "androidSdkVersion": 35, + "hl": "en-GB", + "gl": "US", + "utcOffsetMinutes": 0 + }, + "request": { + "internalExperimentFlags": [], + "useSsl": true + }, + "user": { + "lockedSafetyMode": false + } + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/res/request2.json b/Tesses.YouTubeDownloader/res/request2.json new file mode 100644 index 0000000..ed670bb --- /dev/null +++ b/Tesses.YouTubeDownloader/res/request2.json @@ -0,0 +1,31 @@ +{ + "context": { + "client": { + "clientName": "ANDROID", + "clientVersion": "19.28.35", + "clientScreen": "WATCH", + "platform": "MOBILE", + "visitorData": "VISITOR_DATA", + "osName": "Android", + "osVersion": "15", + "androidSdkVersion": 35, + "hl": "en-GB", + "gl": "US", + "utcOffsetMinutes": 0 + }, + "request": { + "internalExperimentFlags": [], + "useSsl": true + }, + "user": { + "lockedSafetyMode": false + } + }, + "playerRequest": { + "videoId": "VIDEO_ID_HERE", + "cpn": "kDqPxSobYp3UbLvx", + "contentCheckOk": true, + "racyCheckOk": true + }, + "disablePlayerResponse": false +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/Bell.tcross b/Tesses.YouTubeDownloader/src/Bell.tcross new file mode 100644 index 0000000..b5a39ac --- /dev/null +++ b/Tesses.YouTubeDownloader/src/Bell.tcross @@ -0,0 +1,29 @@ +class SubscriptionBell +{ + /^ Disabled bell ^/ + static getDisabled() "Disabled"; + /^ Download (Low quality) ^/ + static getDownloadLow() "DownloadLow"; + /^ Download (High quality) ^/ + static getDownloadHigh() "DownloadHigh"; + /^ Download (and notify) (Low quality) ^/ + static getBellLow() "BellLow"; + /^ Download (and notify) (High quality) ^/ + static getBellHigh() "BellHigh"; + + /^ Notify ^/ + static getBell() "Bell"; + + static getBells() + { + return [ + {name="Disabled",value=SubscriptionBell.Disabled}, + {name="Notify",value=SubscriptionBell.Bell}, + {name="Notify and Download (Low)", value=SubscriptionBell.BellLow}, + {name="Notify and Download (High)", value=SubscriptionBell.BellHigh}, + {name="Download (Low)", value=SubscriptionBell.DownloadLow}, + {name="Download (High)", value=SubscriptionBell.DownloadHigh} + + ]; + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/Event.tcross b/Tesses.YouTubeDownloader/src/Event.tcross new file mode 100644 index 0000000..3d0dd5b --- /dev/null +++ b/Tesses.YouTubeDownloader/src/Event.tcross @@ -0,0 +1,27 @@ +class TYTD.Event { + private _events = []; + + public operator+(e) + { + if (!_events.Contains(e)) + { + this._events.Add(e); + } + return this; + } + + public operator-(e) + { + this._events.Remove(e); + return this; + } + + + public Invoke($$args) + { + each(var e : this._events) + { + e.Call(args); + } + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/Music.tcross b/Tesses.YouTubeDownloader/src/Music.tcross new file mode 100644 index 0000000..e8f5e6d --- /dev/null +++ b/Tesses.YouTubeDownloader/src/Music.tcross @@ -0,0 +1,127 @@ +class TYTD.Music +{ + public getUserAgent() $"TYTDMusic/{this.getUserAgent.File.Version.Major}.{this.getUserAgent.File.Version.Minor}.{this.getUserAgent.File.Version.Patch} ( tesses@tesses.net )"; + public TYTD; + public Music(tytd) + { + this.TYTD = tytd; + TYTD.Storage.CreateDirectory(/"Album Arts"); + } + + public GetArtists(query) + { + + var req = { + RequestHeaders = [ + { + Key = "User-Agent", + Value = this.UserAgent + } + ] + }; + var resp = Net.Http.MakeRequest($"https://musicbrainz.org/ws/2/artist?query={Net.Http.UrlEncode(query)}&fmt=json",req); + + if(resp.StatusCode >= 200 && resp.StatusCode <= 299) + { + var jo = Json.Decode(resp.ReadAsString()); + var items = []; + each(var item : jo.artists) + { + items.Add({ + id = item.id, + type = item.type, + name = item.name + }); + } + + return items; + } + + return null; + } + public GetArtistsAlbums(artist_id) + { + var req = { + RequestHeaders = [ + { + Key = "User-Agent", + Value = this.UserAgent + } + ] + }; + var resp = Net.Http.MakeRequest($"https://musicbrainz.org/ws/2/artist/{Net.Http.UrlPathEncode(artist_id)}?inc=artist-credits+releases&fmt=json",req); + if(resp.StatusCode >= 200 && resp.StatusCode <= 299) + { + var jo = Json.Decode(resp.ReadAsString()); + var items = []; + each(var item : jo.releases) + { + items.Add({ + id = item.id, + packaging = item.packaging, + title = item.title, + date = item.date + }); + } + + return {items,name=jo.name}; + } + + return null; + } + public GetAlbumArts(album_id) + { + var req = { + RequestHeaders = [ + { + Key = "User-Agent", + Value = UserAgent + } + ], + FollowRedirects=true + }; + var url = $"https://coverartarchive.org/release/{Net.Http.UrlPathEncode(album_id)}"; + var resp = Net.Http.MakeRequest(url,req); + if(resp.StatusCode >= 200 && resp.StatusCode <= 299) + { + return Json.Decode(resp.ReadAsString()); + } + return null; + } + + public GetAlbumArt(album_id, res) + { + var path = /"Album Arts"/$"{album_id}-{res}.jpg"; + if(TYTD.Storage.FileExists(path)) return FS.ReadAllBytes(TYTD.Storage,path); + var arts = GetAlbumArts(album_id); + if(arts == null) return null; + + each(var img : arts.images) + { + if(img.front) + { + var url = res == "full" ? img.image : img.thumbnails.[res]; + var req = { + RequestHeaders = [ + { + Key = "User-Agent", + Value = UserAgent + } + ], + FollowRedirects=true + }; + var resp = Net.Http.MakeRequest(url.Replace("http://","https://"),req); + if(resp.StatusCode >= 200 && resp.StatusCode <= 299) + { + var strm = TYTD.Storage.OpenFile(path,"wb"); + resp.CopyToStream(strm); + strm.Close(); + return FS.ReadAllBytes(TYTD.Storage,path); + } + + + } + } + return null; + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/Queue.tcross b/Tesses.YouTubeDownloader/src/Queue.tcross new file mode 100644 index 0000000..fd68bb6 --- /dev/null +++ b/Tesses.YouTubeDownloader/src/Queue.tcross @@ -0,0 +1,35 @@ +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(); + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/Resolution.tcross b/Tesses.YouTubeDownloader/src/Resolution.tcross new file mode 100644 index 0000000..d2af79d --- /dev/null +++ b/Tesses.YouTubeDownloader/src/Resolution.tcross @@ -0,0 +1,36 @@ +class Resolution +{ + /^ Used to fetch metadata only ^/ + 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) ^/ + static getMP3() "MP3"; + /^ Get the highest audio 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"; + + 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} + ]; + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/VideoDownload.tcross b/Tesses.YouTubeDownloader/src/VideoDownload.tcross new file mode 100644 index 0000000..6b5c1fc --- /dev/null +++ b/Tesses.YouTubeDownloader/src/VideoDownload.tcross @@ -0,0 +1,7 @@ +class IVideoDownload { + public setTYTD(tytd); + public setProgress(p); + public getVideo(); + public abstract Start(); +} + diff --git a/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross b/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross new file mode 100644 index 0000000..4cc1854 --- /dev/null +++ b/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross @@ -0,0 +1,1550 @@ +class TYTD.Downloader { + public Storage; + public DatabaseDirectory; + public PackageManager = new Tesses.CrossLang.PackageManager(); + + public Servers = Net.Http.MountableServer({Handle=(ctx)=>false}); + + public Downloader(vfs,dbDir) + { + this.Storage = vfs; + this.DatabaseDirectory = dbDir; + } + public DownloadVideo(id,$res) + { + switch(res) + { + case Resolution.NoDownload: + { + var id = TYTD.GetVideoId(id); + if(id != null) + PutVideoInfoIfNotExists(id); + } + 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; + } + } + + public DownloadPlaylist(id,$res) + { + var pid = TYTD.GetPlaylistId(id); + + + if(pid != null) + { + this.PlaylistQueue.Push(()=>{ + each(var item : this.QueryPlaylistItems(pid,true)) + { + each(var vid : item) + { + + DownloadVideo(vid, res); + } + } + }); + } + } + + public DownloadChannel(id,$res) + { + var cid = TYTD.GetChannelId(id); + + if(cid != null) + { + this.PlaylistQueue.Push(()=>{ + each(var item : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false)) + { + each(var vid : item) + { + DownloadVideo(vid, res); + } + } + }); + } + } + + public GetPluginThumbnail(name) + { + each(var item : this.Plugins) + { + if(item.pluginName == name) + return item.pluginIcon; + } + + return embed("package_icon.png"); + } + + + public DownloadItem(url, $res) + { + var vid = TYTD.GetVideoId(url); + var pid = TYTD.GetPlaylistId(url); + + var cid = TYTD.GetChannelId(url); + + if(vid != null && url.Length == 11) + { + this.DownloadVideo(vid, res); + } + else if(pid != null) + { + this.DownloadPlaylist(pid,res); + } + else if(vid != null) + { + this.DownloadVideo(vid, res); + } + else if(cid != null) + { + this.DownloadChannel(cid,res); + } + + } + + public PageRedirect(url) + { + var vid = TYTD.GetVideoId(url); + + if(vid != null && url.Length == 11) + { + return $"./video?v={Net.Http.UrlEncode(vid)}"; + } + return "./"; + } + + public GetDownloadPath(url) + { + //Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS downloads (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, name TEXT, mime TEXT);"); + this.Mutex.Lock(); + var db = this.OpenDB(); + var res = Sqlite.Exec(db, $"SELECT * FROM downloads WHERE url = {Sqlite.Escape(url)};"); + + Sqlite.Close(db); + this.Mutex.Unlock(); + + if(TypeOf(res) == "List" && res.Length > 0) + { + return /"Downloads"/$"{res[0].id}.bin"; + } + return null; + } + + private Views2Str(views) + { + if(views == 1) return "1 view"; + if(views < 1000) return $"{views} views"; + if(views < 1000000) return $"{views/1000}K views"; + if(views < 1000000000) return $"{views/1000000}M views"; + if(views < 1000000000000) return $"{views/1000000000}B views"; + return $"{views/1000000000000}T views"; + } + + + public GetVideos(query, offset, count) + { + this.Muxex.Lock(); + var db = this.OpenDB(); + var q = Sqlite.Escape($"%{query}%"); + var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.title LIKE {q} OR v.shortDescription LIKE {q}) LIMIT {count} OFFSET {offset*count};"); + + + Sqlite.Close(db); + + this.Mutex.Unlock(); + if(TypeOf(res) != "List") throw res; + + each(var item : res) + { + if(item.keywords != "undefined") + item.keywords = Json.Decode(item.keywords); + else item.keywords = []; + + item.addDate = ParseLong(item.addDate); + item.lengthSeconds = ParseLong(item.lengthSeconds); + item.viewCount = ParseLong(item.viewCount); + item.viewCountStr = this.Views2Str(item.viewCount); + + } + return res; + } + + public GetPlaylists(query, offset, count) + { + this.Muxex.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};"); + + Sqlite.Close(db); + + this.Mutex.Unlock(); + if(TypeOf(res) != "List") throw res; + + return res; + } + public GetPlaylistContents(id, offset, count) + { + id = TYTD.GetPlaylistId(id); + if(id == null) return { + title = "N/A", + channelId="", + channelTitle="N/A", + items = [] + }; + this.Muxex.Lock(); + var db = this.OpenDB(); + var channelTitle = ""; + var title = ""; + var channelId = ""; + /* + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS videos (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT UNIQUE, title TEXT, lengthSeconds INTEGER, keywords TEXT, channelId TEXT, shortDescription TEXT, viewCount INTEGER, author TEXT, addDate INTEGER, tytdTag TEXT);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlists (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId TEXT UNIQUE,channelId TEXT,channelTitle TEXT, title TEXT);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS channels (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE, title TEXT);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS downloads (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, name TEXT, mime TEXT);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlist_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId INTEGER, videoId TEXT);"); + + */ + + var res = Sqlite.Exec(db, $"SELECT * FROM playlists WHERE playlistId = {Sqlite.Escape(id)};"); + var res2 = null; + if(TypeOf(res) == "List" && res.Length > 0) + { + channelId = res[0].channelId; + channelTitle = res[0].channelTitle; + title = res[0].title; + var id = res[0].id; + res2 = Sqlite.Exec(db, $"SELECT * FROM playlist_entries e INNER JOIN videos v ON e.videoId = v.videoId WHERE e.playlistId = {id};"); + } + + Sqlite.Close(db); + + this.Mutex.Unlock(); + + + if(TypeOf(res) != "List") throw res; + + if(TypeOf(res2) != "List") throw res2; + each(var item : res2) + { + if(item.keywords != "undefined") + item.keywords = Json.Decode(item.keywords); + else + item.keywords = []; + item.addDate = ParseLong(item.addDate); + item.lengthSeconds = ParseLong(item.lengthSeconds); + item.viewCount = ParseLong(item.viewCount); + item.viewCountStr = this.Views2Str(item.viewCount); + } + return { + channelTitle, + title, + channelId, + items = res2 + }; + } + public GetChannelContents(id, offset, count) + { + id = TYTD.GetChannelId(id); + if(id == null) return { + authorName = "N/A", + items = [] + }; + this.Muxex.Lock(); + var db = this.OpenDB(); + var authorName = ""; + + var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.channelId = {Sqlite.Escape(id)}) LIMIT {count} OFFSET {offset*count};"); + if(TypeOf(res) != "List" || res.Length == 0) { + var res2 = Sqlite.Exec(db,"SELECT * FROM channels c WHERE c.channelId = {Sqlite.Escape(id)};"); + if(TypeOf(res2) == "List" && res2.Length > 0) + { + authorName = res2[0].title; + } + } + else { + authorName = res[0].author; + } + Sqlite.Close(db); + + this.Mutex.Unlock(); + if(TypeOf(res) != "List") throw res; + each(var item : res) + { + if(item.keywords != "undefined") + item.keywords = Json.Decode(item.keywords); + else + item.keywords = []; + item.addDate = ParseLong(item.addDate); + item.lengthSeconds = ParseLong(item.lengthSeconds); + item.viewCount = ParseLong(item.viewCount); + item.viewCountStr = this.Views2Str(item.viewCount); + } + return { + authorName, + items = res + }; + } + + public GetChannels(query, offset, count) + { + this.Muxex.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};"); + + Sqlite.Close(db); + + this.Mutex.Unlock(); + if(TypeOf(res) != "List") throw res; + + return res; + } + + public GetVideoPath(v,res) + { + var id = TYTD.GetVideoId(v); + if(id == null) throw "No id specified"; + var dir = /"Streams"/id.Substring(0,4)/id.Substring(4); + + switch(res) + { + case Resolution.LowVideo: + return dir / "ytmux.mp4"; + break; + case Resolution.VideoOnly: + return dir / "vo.bin"; + break; + + case Resolution.AudioOnly: + return dir / "ao.bin"; + break; + case Resolution.MP4: + return dir / "conv.mp4"; + break; + case Resolution.MKV: + return dir / "conv.mkv"; + break; + case Resolution.MP3: + return dir / "conv.mp3"; + break; + case Resolution.FLAC: + return dir / "conv.flac"; + break; + } + throw $"Could not get file path for format {res}"; + } + + + + public GetVideo(vid) + { + var id = TYTD.GetVideoId(vid); + if(id == null) return null; + this.Muxex.Lock(); + var db = this.OpenDB(); + var res = Sqlite.Exec(db, $"SELECT * FROM videos WHERE videoId = {Sqlite.Escape(id)};"); + + var out = null; + if(TypeOf(res) == "List" && res.Length == 1) out = res[0]; + + + Sqlite.Close(db); + this.Mutex.Unlock(); + if(out != null) + { + if(out != "undefined") + out.keywords = Json.Decode(out.keywords); + else + out.keywords = []; + out.addDate = ParseLong(out.addDate); + out.lengthSeconds = ParseLong(out.lengthSeconds); + out.viewCount = ParseLong(out.viewCount); + out.viewCountStr = this.Views2Str(out.viewCount); + } + return out; + } + + + + public GetPlaylist(id) + { + var id = TYTD.GetPlaylistId(vid); + if(id == null) return null; + this.Muxex.Lock(); + var db = this.OpenDB(); + var res = Sqlite.Exec(db, $"SELECT * FROM playlists WHERE playlistId = {Sqlite.Escape(id)};"); + + var out = null; + if(TypeOf(res) == "List" && res.Length == 1) out = res[0]; + + + Sqlite.Close(db); + this.Mutex.Unlock(); + + return out; + } + + public GetChannel(id) + { + var id = TYTD.GetChannelId(vid); + if(id == null) return null; + this.Muxex.Lock(); + var db = this.OpenDB(); + var res = Sqlite.Exec(db, $"SELECT * FROM channels WHERE channelId = {Sqlite.Escape(id)};"); + + var out = null; + if(TypeOf(res) == "List" && res.Length == 1) out = res[0]; + + + Sqlite.Close(db); + this.Mutex.Unlock(); + + return out; + } + + /^ Get the list of personal list names^/ + public GetPersonalLists() + { + this.Muxex.Lock(); + var db = this.OpenDB(); + var items = []; + + var lists = Sqlite.Exec(db, "SELECT * FROM personal_lists;"); + if(TypeOf(lists) == "List") + { + each(var item : lists) + { + items.Add(item); + var res2=Sqlite.Exec(db, $"SELECT * FROM personal_list_entries WHERE listName = {Sqlite.Escape(item.name)} LIMIT 1;"); + if(TypeOf(res2) == "List" && res2.Length > 0) item.firstVideo = res2[0].videoId; + } + } + + Sqlite.Close(db); + this.Mutex.Unlock(); + return items; + } + public SetPersonalListDescription(name,description) + { + this.Muxex.Lock(); + var db = this.OpenDB(); + + Sqlite.Exec(db, $"UPDATE personal_lists SET description = {Sqlite.Escape(description)} WHERE name = {Sqlite.Escape(name)};"); + Sqlite.Close(db); + this.Mutex.Unlock(); + } + public GetPersonalListDescription(name) + { + this.Muxex.Lock(); + var db = this.OpenDB(); + + var res = Sqlite.Exec(db, $"SELECT * FROM personal_lists WHERE name = {Sqlite.Escape(name)};"); + Sqlite.Close(db); + this.Mutex.Unlock(); + + if(TypeOf(res) == "List" && res.Length > 0) + { + res[0].description; + } + return ""; + } + public GetPersonalListContents(name, offset, count) + { + this.Muxex.Lock(); + var db = this.OpenDB(); + var items = []; + + var lists = Sqlite.Exec(db, $"SELECT * FROM personal_list_entries e INNER JOIN videos v ON e.videoId = v.videoId WHERE e.listName = {Sqlite.Escape(name)} LIMIT {count} OFFSET {offset*count};"); + + if(TypeOf(lists) == "List") + { + each(var item : lists) + { + if(item.keywords != "undefined") + item.keywords = Json.Decode(item.keywords); + else item.keywords = []; + item.addDate = ParseLong(item.addDate); + item.lengthSeconds = ParseLong(item.lengthSeconds); + item.viewCount = ParseLong(item.viewCount); + item.viewCountStr = this.Views2Str(item.viewCount); + items.Add(item); + } + } + + Sqlite.Close(db); + this.Mutex.Unlock(); + + return items; + } + + public AddToPersonalList(name, id) + { + id = TYTD.GetVideoId(id); + + if(id == null) return; + this.Mutex.Lock(); + var db = this.OpenDB(); + //personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT) + Sqlite.Exec(db, $"INSERT INTO personal_lists (name) VALUES ({Sqlite.Escape(name)});"); + Sqlite.Exec(db, $"INSERT INTO personal_list_entries (listName,videoId) VALUES ({Sqlite.Escape(name)},{Sqlite.Escape(id)});"); + Sqlite.Close(db); + this.Mutex.Unlock(); + } + public RemoveFromPersonalList(name,id) + { + id = TYTD.GetVideoId(id); + if(id == null) return; + this.Mutex.Lock(); + var db = this.OpenDB(); + //personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT) + Sqlite.Exec(db, $"DELETE FROM personal_list_entries e WHERE (e.listName = {Sqlite.Escape(name)} AND e.videoId = {Sqlite.Escape(id)});"); + Sqlite.Close(db); + this.Mutex.Unlock(); + } + + public SetSubscriptionBell(url, bell) + { + var cid = TYTD.GetChannelId(url); + if(cid == null) return; + + //Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);"); + this.Mutex.Lock(); + var db = this.OpenDB(); + if(bell == null) + Sqlite.Exec(db,$"DELETE FROM subscriptions WHERE channelId = {Sqlite.Escape(cid)};"); + else + Sqlite.Exec(db,$"INSERT INTO subscriptions (channelId,bell) VALUES ({Sqlite.Escape(cid)},{Sqlite.Escape(bell)});"); + Sqlite.Close(db); + this.Mutex.Unlock(); + + } + + public GetSubscriptionBell(url) + { + var cid = TYTD.GetChannelId(url); + if(cid == null) return null; + //Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);"); + this.Mutex.Lock(); + var db = this.OpenDB(); + var res = Sqlite.Exec(db,$"SELECT * FROM subscriptions WHERE channelId = {Sqlite.Escape(cid)};"); + Sqlite.Close(db); + this.Mutex.Unlock(); + if(TypeOf(res) == "List" && res.Count > 0) return res[0].bell; + return null; + } + + public GetSubscriptionUrls() + { + var cid = TYTD.GetChannelId(url); + if(cid == null) return []; + //Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);"); + this.Mutex.Lock(); + var db = this.OpenDB(); + var res = Sqlite.Exec(db,"SELECT * FROM subscriptions;"); + Sqlite.Close(db); + this.Mutex.Unlock(); + var list=[]; + if(TypeOf(res) == "List" && res.Count > 0) { + each(var i : res) + { + list.Add(i.channelId); + } + } + return list; + } + + public RemoveSubscription(url) + { + SetSubscriptionBell(url,null); + } + + + public VideoStarted = new TYTD.Event(); + + public VideoProgress = new TYTD.Event(); + + public CurrentVideo = { + Title = "N/A", + Channel = "N/A", + VideoId = "", + ChannelId = "" + }; + + public CurrentVideoProgress = 0.0; + + public VideoEnded = new TYTD.Event(); + + public Bell = new TYTD.Event(); + + public Plugins = []; + + + + public Mutex = new Mutex(); + + public Running=true; + + private DownloaderThreadHandle; + + private Queue = new TYTD.Queue(); + + private PlaylistThreadHandle; + + private PlaylistQueue = new TYTD.Queue(); + + public Config = { + TYTDTag = "UnknownPC", + BellTimer = 10800, + EnablePlugins=true + }; + public SaveConfig() + { + this._setPluginValue("","settings", Json.Encode(this.Config)); + } + + public GetPlaylistThumbnail(id,res) + { + var id = TYTD.GetPlaylistId(id); + if(id == null) return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg"); + 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;"); + + this.Mutex.Unlock(); + if(TypeOf(_res) == "List" && _res.Length > 0) + { + var vid = _res[0].videoId; + + return FS.ReadAllBytes(this.Storage,TryDownloadVideoThumbnail(vid,res)); + } + return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg"); + } + public GetChannelThumbnail(id,res) + { + var id = TYTD.GetChannelId(id); + if(id == null) return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg"); + this.Mutex.Lock(); + var db = this.OpenDB(); + var _res = Sqlite.Exec(db,$"SELECT * FROM videos WHERE channelId = {Sqlite.Escape(id)} LIMIT 1;"); + + this.Mutex.Unlock(); + if(TypeOf(_res) == "List" && _res.Length > 0) + { + var vid = _res[0].videoId; + + return FS.ReadAllBytes(this.Storage,TryDownloadVideoThumbnail(vid,res)); + } + return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg"); + } + private LoadPlugin(path) + { + //{name,version, pluginEnv, pluginObject, pluginIcon, pluginName, info} + var strm = this.Storage.OpenFile(path,"rb"); + var exec = VM.LoadExecutable(strm); + strm.Close(); + var name = exec.Name; + var version = exec.Version; + + func loadExec(_pkg, _exec, _path) + { + var subdir = new SubdirFilesystem(this.Storage,_path.GetParent()); + var info = {}; + try {info = Json.Decode(_exec.Info);} catch(ex) {} + _pkg.info = info; + + _pkg.pluginName = TypeOf(info.short_name) ? info.short_name : name; + var reso = _exec.Resources; + var ico = _exec.Icon; + _pkg.pluginIcon = ico >= 0 && i < reso.Count ? reso[ico] : embed("package_icon.png"); + subdir.CreateDirectory(/"Files"); + var d = { + TYTD = { + Downloader = this, + GetVideoId = TYTD.GetVideoId, + GetPlaylistId = TYTD.GetPlaylistId, + GetChannelId = TYTD.GetChannelId, + Config = { + GetAt = (key)=>{ + return this._getPluginValue(info.short_name, key); + }, + SetAt = (key,value)=>{ + var value=value.ToString(); + this._setPluginValue(info.short_name,key,value); + return value; + }, + Directory = new SubdirFilesystem(subdir, /"Files"), + DirectoryPath = this.DatabaseDirectory / "Plugins" / _path.GetParent().GetFileName() / "Files" + } + }, + Resolution = Resolution, + SubscriptionBell = SubscriptionBell + + + }; + var env = VM.CreateEnvironment(d); + + try{ + env.RegisterEverything(); + }catch(ex) Console.WriteLine(ex); + + + env.LockRegister(); + env.LoadFileWithDependencies(subdir, _exec); + + _pkg.pluginObject = d.PluginInit(); + if(_pkg.pluginObject.Server != null && _pkg.pluginObject.Server != undefined) + { + var path = /"plugin"/_pkg.pluginName; + + this.Servers.Mount(path,_pkg.pluginObject.Server); + } + + _pkg.pluginEnv = env; + + + } + each(var pkg : this.Plugins) + { + if(pkg.name == name) { + if(pkg.version >= version) return; + pkg.Close(); + pkg.version = version; + + + loadExec(pkg, exec, path); + return; + } + } + var pkg2 = {name,version}; + func _close() + { + if(pkg2.pluginObject.Server != null && pkg2.pluginObject.Server != undefined) + { + var path = /"plugin"/pkg2.pluginName; + this.Servers.Unmount(path); + } + + pkg2.pluginObject.Close(); + } + pkg2.Close = _close; + loadExec(pkg2, exec, path); + this.Plugins.Add(pkg2); + } + + public LoadPlugins() + { + if(this.Config.EnablePlugins) + { + var dir = /"Plugins"; + each(var item : this.Storage.EnumeratePaths(dir)) + { + if(this.Storage.DirectoryExists(item)) + { + LoadPlugin(item/$"{item.GetFileName()}.crvm"); + } + } + } + } + public Start() + { + this.Storage.CreateDirectory(/"Downloads"); + this.Storage.CreateDirectory(/"Streams"); + this.Storage.CreateDirectory(/"Plugins"); + if(!this.Storage.FileExists(/"Streams"/"nullthumb.jpg")) + { + var resp=Net.Http.MakeRequest("https://s.ytimg.com/vi/0/0.jpg"); + var strm = this.Storage.OpenFile(/"Streams"/"nullthumb.jpg","wb"); + resp.CopyToStream(strm); + resp.Close(); + } + this.InitDatabase(); + this.DownloadThreadHandle= new Thread(this.DownloadThread); + this.PlaylistThreadHandle = new Thread(this.PlaylistThread); + + this.LoadPlugins(); + } + private lastSubPollTime = 0; + private PlaylistThread() + { + while(this.Running) + { + var res = this.PlaylistQueue.Pop(); + + if(TypeOf(res) != "Null") + { + res(); + } + + var currentTime = DateTime.NowEpoch; + var bt = this.Config.BellTimer; + + + if((currentTime-this.lastSubPollTime) > bt) + { + this.lastSubPollTime = currentTime; + + + this.Mutex.Lock(); + var db = this.OpenDB(); + var res = Sqlite.Exec(db, "SELECT * FROM subscriptions;"); + //Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);"); + + this.Mutex.Unlock(); + /* + /^ Disabled bell ^/ + static getDisabled() "Disabled"; + /^ Download (Low quality) ^/ + static getDownloadLow() "DownloadLow"; + /^ Download (High quality) ^/ + static getDownloadHigh() "DownloadHigh"; + /^ Download (and notify) (Low quality) ^/ + static getBellLow() "BellLow"; + /^ Download (and notify) (High quality) ^/ + static getBellHigh() "BellHigh"; + + /^ Notify ^/ + static getBell() "Bell"; + */ + + each(var sub : res) + { + + var downloadRes = Resolution.NoDownload; + var notify = false; + switch(sub.bell) + { + case SubscriptionBell.Bell: + notify=true; + break; + case SubscriptionBell.BellLow: + notify=true; + case SubscriptionBell.DownloadLow: + downloadRes = Resolution.LowVideo; + break; + case SubscriptionBell.BellHigh: + notify = true; + break; + case SubscriptionBell.DownloadHigh: + downloadRes = Resolution.MKV; + break; + } + if(!notify && downloadRes == Resolution.NoDownload) continue; + var cid = sub.channelId; + + var newVideos = []; + + each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false)) + { + each(var videoId : batch) + { + + if(this.GetVideo(videoId) == null) + newVideos.Add(videoId); + } + } + + if(notify) + { + each(var id : newVideos) + { + this.PutVideoInfoIfNotExists(id); + var res = this.GetVideo(id); + if(res != null) + this.Bell.Invoke(this,{ + Video = res + }); + + } + } + if(downloadRes != Resolution.NoDownload) + { + each(var id : newVideos) + { + this.DownloadVideo(id,downloadRes); + } + } + + } + } + } + } + + private DownloadThread() + { + while(this.Running) + { + var res = this.Queue.Pop(); + + if(TypeOf(res) != "Null") + { + + 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.VideoEnded.Invoke(this,{ + Video = res.Video + }); + } + } + } + + public Stop() + { + each(var item : this.Plugins) + { + item.Close(); + } + this.Running = false; + this.PlaylistThreadHandle.Join(); + this.DownloaderThreadHandle.Join(); + + } + + /* + public GetChannelThumbnail(channelId) + { + var id = TYTD.GetChannelId(channelId); + var path = /"ChannelThumbnails"/id.Substring(2,4)/id.Substring(6); + if(this.Storage.FileExists(path+".webp")) + { + return { + data = FS.ReadAllBytes(this.Storage,path+".webp"), + mime = "image/webp" + }; + } + else if(this.Storage.FileExists(path+".jpg")) + { + return { + data = FS.ReadAllBytes(this.Storage,path+".jpg"), + mime = "image/jpeg" + }; + } + return null; + }*/ + public TryDownloadVideoThumbnail(v, res) + { + var id = TYTD.GetVideoId(v); + if(TypeOf(id) == "String") + { + var path = /"Streams"/id.Substring(0,4) / id.Substring(4) / $"{res}.jpg"; + this.Storage.CreateDirectory(path.GetParent()); + if(this.Storage.FileExists(path)) + { + return path; + } + else { + var url = $"https://s.ytimg.com/vi/{id}/{res}.jpg"; + Net.Http.DownloadToFile(url,this.Storage, path); + + return path; + } + } + return null; + } + public GetVideoThumbnail(v, res) + { + var thumb = TryDownloadVideoThumbnail(v,res); + if(thumb != null) + { + return FS.ReadAllBytes(this.Storage, thumb); + } + return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg"); + } + private OpenDB() + { + var dbFile = this.DatabaseDirectory / "tytd.db"; + return Sqlite.Open(dbFile); + } + + public PackageState(name, version) + { + each(var item : this.Plugins) + { + if(item.name == name) { + if(item.version < version) return 2; + return 1; + } + } + return 0; + } + private PackageDownload(name,version) + { + var dir = new SubdirFilesystem(this.Storage,/"Plugins"); + this.PackageManager.DownloadPlugin(dir,name,version); + this.LoadPlugins(); + } + public PackageInstall(name, version) + { + + each(var item : this.Plugins) + { + if(item.name == name) + { + + if(item.version >= version) + { + return; + } + + + } + } + + this.PackageDownload(name, version); + } + + public PackageUninstall(name) + { + each(var item : this.Plugins) + { + if(item.name == name) + { + this.Plugins.Remove(item); + + item.Close(); + this.Storage.DeleteDirectoryRecurse(/"Plugins"/item.pluginName); + + break; + } + } + } + + public getTYTDTag() + { + return this.Config.TYTDTag ?? "UnknownPC"; + } + public PutVideoInfoIfNotExists(vid) + { + var id = TYTD.GetVideoId(vid); + if(id != null) + { + var e = this.GetVideo(id); + if(e == null) + { + var req = this.ManifestRequest(id); + this.PutVideoInfo(req.playerResponse.videoDetails); + } + } + + } + + public PutVideoInfo(info) + { + this.Muxex.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); + Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.author)});"); + Sqlite.Close(db); + this.Mutex.Unlock(); + } + + private InitDatabase() + { + this.Muxex.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);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS channels (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE, title TEXT);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlist_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId INTEGER, videoId TEXT);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_list_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, listName TEXT, videoId TEXT);"); + 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);"); + var config=Sqlite.Exec(db,"SELECT * FROM plugin_settings WHERE extension = '' AND key = 'settings';"); + if(TypeOf(config) == "List" && config.Length>0) + { + try { + this.Config = Json.Decode(config[0].value); + }catch(ex) { + + } + } + Sqlite.Close(db); + this.Mutex.Unlock(); + + + } + private _setPluginValue(extension,key,value) + { + this.Muxex.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.Close(db); + this.Mutex.Unlock(); + } + private _getPluginValue(extension,key) + { + this.Mutex.Lock(); + var db = this.OpenDB(); + var config=Sqlite.Exec(db,$"SELECT * FROM plugin_settings WHERE extension = {Sqlite.Escape(extension)} AND key = {Sqlite.Escape(key)};"); + + Sqlite.Close(db); + this.Mutex.Unlock(); + + if(TypeOf(config) == "List" && config.Length>0) + { + return config[0].value; + } + } + private DownloadChannelThumbInternal(channelId, thumbnail_url) + { + var id = TYTD.GetChannelId(channelId); + var path = /"ChannelThumbnails"/id.Substring(2,4)/id.Substring(6); + this.Storage.CreateDirectory(path.GetParent()); + if(!(this.Storage.FileExists(path + ".webp") || this.Storage.FileExists(path+".jpg"))) + { + if(thumbnail_url.StartsWith("//")) thumbnail_url = $"https:{thumbnail_url}"; + var dl = Net.Http.MakeRequest(thumbnail_url); + + if(dl.StatusCode >= 200 && dl.StatusCode <= 299) + { + var ct=dl.ResponseHeaders.TryGetFirst("Content-Type"); + if(TypeOf(ct) == "String") + { + var s = ct.Split("; ",true,2); + var ext = ".jpg"; + if(s[0] == "image/webp") + ext = ".webp"; + + var dest = this.Storage.OpenFile(path+ext,"wb"); + dl.CopyToStream(dest); + dest.Close(); + } + } + } + + } + private DiscoverInternal(q,continuation,params) + { + if(continuation == undefined) var continuation = null; + var jo = { + query = q, + params, + continuation, + request = { + internalExperimentFlags=[], + useSsl=true + }, + user = { + lockedSafetyMode=false + }, + + context = { + client = { + clientName = "WEB", + clientVersion = "2.20250710.09.00", + hl = "en-US", + gl = "US", + platform = "DESKTOP", + originalUrl = "https://www.youtube.com", + utcOffsetMinutes = 0 + } + } + }; + + var url = $"https://www.youtube.com/youtubei/v1/search?key=AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w&prettyPrint=false"; + var requestData = { + Method = "POST", + RequestHeaders = [ + { + Key = "User-Agent", + Value = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36" + }, + { + Key="Cookie", + Value = "SOCS: CAISEwgDEgk2NzM5OTg2ODUaAmVuIAEaBgiA6p23Bg" + }, + { + Key="Referer", + Value="https://www.youtube.com" + }, + { + Key="Origin", + Value="https://www.youtube.com" + }, + { + Key="X-YouTube-Client-Version", + Value="2.20250710.09.00" + }, + { + Key="X-YouTube-Client-Name", + Value="1" + } + ], + Body = Net.Http.TextHttpRequestBody(jo.ToString(),"application/json") + }; + var resp = Net.Http.MakeRequest(url,requestData); + if(resp.StatusCode < 200 || resp.StatusCode > 299) return null; + + var jo2 = Json.Decode(resp.ReadAsString()); + var o = jo2.contents; + if(TypeOf(o) != "Dictionary") o = jo2.onResponseReceivedCommands; + return o; + } + public DiscoverVideosBasic(q,$continuation) + { + var o = this.DiscoverInternal(q,continuation,"EgIQAQ=="); + if(o == null) return null; + var videos = Dictionary.FindByKey(o,"videoRenderer"); + var items=[]; + each(var item : videos) + { + items.Add({ + id = item.videoId, + title = item.title.runs[0].text + }); + } + return items; + } + public Discover(q,$continuation) + { + var o = this.DiscoverInternal(q,continuation,null); + if(o == null) return null; + + + + + var videos = Dictionary.FindByKey(o,"videoRenderer"); + var playlists = Dictionary.FindByKey(o,"lockupViewModel"); + if(TypeOf(playlists) != "List" || playlists.Length == 0) playlists = Dictionary.FindByKey(o,"playlistRenderer"); + var channels = Dictionary.FindByKey(o,"channelRenderer"); + var items=[]; + + for(var i = 0; i < videos.Length; i++) + { + TryDownloadVideoThumbnail(videos[i].videoId,"0"); + items.Add({ + id = videos[i].videoId, + title = videos[i].title.runs[0].text, + type = "video", + author = videos[i].ownerText.runs, + views = videos[i].viewCountText.simpleText, + uploaded = videos[i].publishedTimeText.simpleText + }); + } + for(var i = 0; i < playlists.Length; i++) + { + items.Add({ + item = playlists[i], + type="playlist" + }); + } + for(var i = 0; i < channels.Length; i++) + { + var thumbnail_url = ""; + var thumbnail_width = 0; + var thumbnail_height = 0; + var description = ""; + + each(var item : channels[i].thumbnail.thumbnails) + { + if(item.width > thumbnail_width && item.height > thumbnail_height) + { + thumbnail_url = item.url; + thumbnail_width = item.width; + thumbnail_height = item.height; + } + } + each(var item : channels[i].descriptionSnippet.runs) + { + description += item.text; + } + var channelId = channels[i].channelId; + + this.DownloadChannelThumbInternal(channelId, thumbnail_url); + + + items.Add({ + id=channelId, + title = channels[i].title.simpleText, + type="channel", + subs = channels[i].videoCountText.simpleText, + description + }); + } + return {items}; + } + + public ManifestRequest(vid) + { + var id = TYTD.GetVideoId(vid); + if(id == null) return null; + TryDownloadVideoThumbnail(id,"0"); + TryDownloadVideoThumbnail(id,"1"); + TryDownloadVideoThumbnail(id,"2"); + TryDownloadVideoThumbnail(id,"3"); + TryDownloadVideoThumbnail(id,"sddefault"); + TryDownloadVideoThumbnail(id,"hqdefault"); + TryDownloadVideoThumbnail(id,"mqdefault"); + TryDownloadVideoThumbnail(id,"default"); + TryDownloadVideoThumbnail(id,"maxresdefault"); + var requestData = { + Method = "POST", + RequestHeaders = [ + { + Key = "User-Agent", + + Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip" + } + ], + Body = Net.Http.TextHttpRequestBody(embed("request.json").ToString(),"application/json") + }; + this.RateLimit(); + var response = Net.Http.MakeRequest("https://youtubei.googleapis.com/youtubei/v1/visitor_id?prettyPrint=false",requestData); + if(response.StatusCode != 200) throw "Not success"; + var data = Json.Decode(response.ReadAsString()); + var visitor = data.responseContext.visitorData; + response.Close(); + var url = "https://youtubei.googleapis.com/youtubei/v1/reel/reel_item_watch?prettyPrint=false&t=dQOGvBU_R4ke&id=4eeZWTqq5VE&$fields=playerResponse"; + + requestData = { + Method = "POST", + RequestHeaders = [ + { + Key = "User-Agent", + Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; 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()); + + } + + + private enumerable QueryPlaylistItems(id, isPlaylist) + { + var url = "https://www.youtube.com/youtubei/v1/next?key=AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w"; + func makeRequest(videoId,index,visitorData) + { + var retriesCount = 5; + for(var retriesRemaining = retriesCount; ; retriesRemaining--) + { + var json = { + playlistId = id, + videoId = videoId, + playlistIndex = index, + context = { + client = { + clientName = "WEB", + clientVersion = "2.20210408.08.00", + hl = "en", + gl = "US", + utcOffsetMinutes = 0, + visitorData = visitorData + } + } + }; + + var requestData = { + Method = "POST", + RequestHeaders = [ + { + Key = "User-Agent", + + Value = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36" + }, + { + Key = "Cookie", + Value = "SOCS: CAISEwgDEgk4MTM4MzYzNTIaAmVuIAEaBgiApPzGBg" + } + ], + Body = Net.Http.TextHttpRequestBody(json.ToString(),"application/json") + }; + this.RateLimit(); + var response = Net.Http.MakeRequest(url,requestData); + if(response.StatusCode != 200) throw "Not success"; + var data = Json.Decode(response.ReadAsString()); + var cr = data.contents.twoColumnWatchNextResults.playlist.playlist; + if(cr == null || cr == undefined) + { + if(index > 0 && visitorData != null && retriesRemaining > 0) + continue; + + if(index <= 0 && visitorData == null && retriesRemaining >= retriesCount) + { + Net.Http.MakeRequest($"https://youtube.com/playlist?list={id}"); + continue; + } + + throw $"Playlist '{id}' is not available."; + } + var items = []; + + each(var item : cr.contents) + { + if(item.playlistPanelVideoRenderer != null && item.playlistPanelVideoRenderer != undefined) + { + items.Add({ + videoId = item.playlistPanelVideoRenderer.videoId, + index = item.playlistPanelVideoRenderer.navigationEndpoint.watchEndpoint.index + }); + } + } + + return { + title = cr.title, + channelTitle = cr.shortBylineText.runs[0].text, + channelId = cr.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId, + videos = items, + visitorData = data.responseContext.visitorData + }; + + + + } + } + + var first = true; + + /*this.Muxex.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); + Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.author)});"); + Sqlite.Close(db); + this.Mutex.Unlock();*/ + + var encounteredIds = []; + var lastVideoId=null; + var lastVideoIndex = 0; + var visitorData = null; + var dbRow = null; + + do(true) + { + var resp = makeRequest(lastVideoId,lastVideoIndex,visitorData); + /* + return { + title = cr.title, + channelTitle = cr.shortBylineText.runs[0].text, + channelId = cr.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId, + videos = items, + visitorData = data.responseContext.visitorData + }; + */ + if(first) { + this.Muxex.Lock(); + var db = this.OpenDB(); + if(isPlaylist) + { + Sqlite.Exec(db,$"INSERT INTO playlists (playlistId,channelId,channelTitle,title) VALUES ({Sqlite.Escape(id)},{Sqlite.Escape(resp.channelId)},{Sqlite.Escape(resp.channelTitle)},{Sqlite.Escape(resp.title)});"); + var res = Sqlite.Exec(db,$"SELECT * FROM playlists WHERE playlistId = {Sqlite.Escape(id)};"); + if(TypeOf(res) == "List" && res.Count > 0) dbRow=ParseLong(res[0].id); + + if(TypeOf(dbRow) == "Long") + Sqlite.Exec(db,$"DELETE FROM playlist_entries WHERE playlistId = {dbRow};"); + } + Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(resp.channelId)},{Sqlite.Escape(resp.channelTitle)});"); + + + Sqlite.Close(db); + this.Mutex.Unlock(); + first=false; + } + + var ids = []; + + each(var itm : resp.videos) + { + var vid = itm.videoId; + var vidx = itm.index; + + lastVideoId = vid; + lastVideoIndex = vidx; + + + + if(encounteredIds.IndexOf(vid) > -1) continue; + encounteredIds.Add(vid); + ids.Add(vid); + if(TypeOf(dbRow) == "Long") + { + this.Muxex.Lock(); + var db = this.OpenDB(); + Sqlite.Exec(db,$"INSERT INTO playlist_entries (playlistId,videoId) VALUES ({dbRow},{Sqlite.Escape(vid)});"); + Sqlite.Close(db); + this.Mutex.Lock(); + } + + } + + if(ids.Count == 0) break; + yield ids; + if(visitorData == null || visitorData == undefined) + visitorData = resp.visitorData; + } + + } + + private lastRequest = 0; + + private requests = 0; + + private rlm=new Muxex(); + + + private RateLimit() + { + this.rlm.Lock(); + var curRequest = DateTime.NowEpoch; + if((curRequest - this.lastRequest) > 60) + { + this.requests = 0; + } + + this.requests++; + if(this.requests > 5) + { + DateTime.Sleep(25000); + this.requests=0; + curRequest = DateTime.NowEpoch; + } + + this.lastRequest = curRequest; + this.rlm.Unlock(); + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/data.tcross b/Tesses.YouTubeDownloader/src/data.tcross new file mode 100644 index 0000000..e69de29 diff --git a/Tesses.YouTubeDownloader/src/ids.tcross b/Tesses.YouTubeDownloader/src/ids.tcross new file mode 100644 index 0000000..e120754 --- /dev/null +++ b/Tesses.YouTubeDownloader/src/ids.tcross @@ -0,0 +1,87 @@ +func TYTD.GetVideoId(v) +{ + func IsValidId(_v) + { + if(_v.Count != 11) return false; + each(var item : _v) + { + if(!(item.IsLetter() || item.IsDigit() || item == '-' || item == '_')) return false; + } + return true; + } + if(TypeOf(v) != "String") return null; + if(IsValidId(v)) return v; + each(var __re : ["youtube\\..+?/watch.*?v=(.*?)(?:&|/|$)","youtu\\.be/(.*?)(?:\\?|&|/|$)","youtube\\..+?/embed/(.*?)(?:\\?|&|/|$)","youtube\\..+?/shorts/(.*?)(?:\\?|&|/|$)","youtube\\..+?/live/(.*?)(?:\\?|&|/|$)"]) + { + var __r = new Regex(__re); + var __s = __r.Search(v); + if(__s.Count == 2) + { + __r=__s[1].Text; + if(IsValidId(__r)) + { + return __r; + } + } + } + return null; + +} +func TYTD.GetPlaylistId(pid) +{ + func IsValidId(v) + { + if(v.Count < 2) return false; + each(var item : v) + { + if(!(item.IsLetter() || item.IsDigit() || item == '-' || item == '_')) return false; + } + return true; + } + if(TypeOf(pid) != "String") return null; + if(IsValidId(pid)) return pid; + each(var __re : ["youtube\\..+?/playlist.*?list=(.*?)(?:&|/|$)","youtube\\..+?/watch.*?list=(.*?)(?:&|/|$)","youtu\\.be/.*?/.*?list=(.*?)(?:&|/|$)","youtube\\..+?/embed/.*?/.*?list=(.*?)(?:&|/|$)"]) + { + var __r = new Regex(__re); + var __s = __r.Search(pid); + if(__s.Count == 2) + { + __r=__s[1].Text; + if(IsValidId(__r)) + { + return __r; + } + } + } + return null; +} +func TYTD.GetChannelId(cid) +{ + func IsValidId(v) + { + if(v.Count != 24) return false; + if(!v.StartsWith("UC")) return false; + each(var item : v) + { + if(!(item.IsLetter() || item.IsDigit() || item == '-' || item == '_')) return false; + } + return true; + } + if(TypeOf(cid) != "String") return null; + if(IsValidId(cid)) return cid; + each(var __re : ["youtube\\..+?/channel/(.*?)(?:\\?|&|/|$)"]) + { + var __r = new Regex(__re); + var __s = __r.Search(cid); + if(__s.Count == 2) + { + __r=__s[1].Text; + if(IsValidId(__r)) + { + return __r; + } + } + } + return null; + +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross new file mode 100644 index 0000000..f8c210c --- /dev/null +++ b/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross @@ -0,0 +1,119 @@ +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; + 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() + { + 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; + } + } + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross new file mode 100644 index 0000000..7427311 --- /dev/null +++ b/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross @@ -0,0 +1,223 @@ +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; + 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); + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross new file mode 100644 index 0000000..dd90dca --- /dev/null +++ b/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross @@ -0,0 +1,83 @@ +class TYTD.SDVideoDownload : IVideoDownload { + private info; + private tytd; + private progress; + private video_stream_url; + + private done=false; + + public SDVideoDownload(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)/"ytmux.mp4"; + 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; + + this.tytd.PutVideoInfo(req.videoDetails); + + this.video_stream_url = req.streamingData.formats[0].url; + + return tytd; + } + public setProgress(p) + { + this.progress = p; + this.progress(0.0); + } + public getVideo() + { + return this.info; + } + public Start() + { + 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)/"ytmux.mp4"; + 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; + this.video_stream_url = req.streamingData.formats[0].url; + } + } + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/videodownload/transcodeaudio.tcross b/Tesses.YouTubeDownloader/src/videodownload/transcodeaudio.tcross new file mode 100644 index 0000000..b53165d --- /dev/null +++ b/Tesses.YouTubeDownloader/src/videodownload/transcodeaudio.tcross @@ -0,0 +1,54 @@ +class TYTD.TranscodeAudio : IVideoDownload { + private id; + private ncv; + private tytd; + private ext; + + public TranscodeAudio(id,ext) + { + this.id = id; + this.ext = ext; + this.ncv = new TYTD.AOVideoDownload(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.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 out = dir / $"conv{this.ext}"; + if(FS.Local.FileExists(out)) return; + var args=["-y","-i",ao.ToString()]; + + + args.Add("-preset"); + args.Add("ultrafast"); + args.Add(out.ToString()); + + + p.Arguments = args; + if(p.Start()) + p.Join(); + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/videodownload/transcodevideo.tcross b/Tesses.YouTubeDownloader/src/videodownload/transcodevideo.tcross new file mode 100644 index 0000000..4425bcd --- /dev/null +++ b/Tesses.YouTubeDownloader/src/videodownload/transcodevideo.tcross @@ -0,0 +1,63 @@ +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.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 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(); + } +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross new file mode 100644 index 0000000..4e85768 --- /dev/null +++ b/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross @@ -0,0 +1,112 @@ +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; + + 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() + { + 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; + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ec9717a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + tytd: + image: onedev.site.tesses.net/tytd2025:latest + volumes: + - tytd-data:/data + ports: + - "3255:3255" + +volumes: + tytd-data: \ No newline at end of file diff --git a/tytd-musik.svg b/tytd-musik.svg new file mode 100644 index 0000000..f89d509 --- /dev/null +++ b/tytd-musik.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + diff --git a/tytd.svg b/tytd.svg new file mode 100644 index 0000000..db7c33f --- /dev/null +++ b/tytd.svg @@ -0,0 +1,66 @@ + + + + + + + + + + +