Merge pull request #527 from Materialious/feature/transcoding
Dash video downloading
This commit is contained in:
@@ -22,7 +22,8 @@
|
||||
- Sync your watch progress between Invidious sessions.
|
||||
- Watch sync parties!
|
||||
- Mini player.
|
||||
- Silence skipper (Experimental)
|
||||
- Silence skipper (Experimental.)
|
||||
- ffmpeg integration for downloading videos with audio at any quality ([Configuration required](./docs/DOCKER.md#step-7-optional-enabling-downloads)).
|
||||
- Preview video on hover.
|
||||
- Sponsorblock built-in.
|
||||
- Return YouTube dislikes built-in.
|
||||
|
||||
@@ -148,6 +148,9 @@ services:
|
||||
|
||||
# Look at "Overwriting Materialious defaults" for all the accepted values.
|
||||
VITE_DEFAULT_SETTINGS: '{"themeColor": "#2596be","region": "US"}'
|
||||
|
||||
# Please look at step 7 about enabling this.
|
||||
VITE_DEFAULT_DOWNLOAD_ENABLED: false
|
||||
```
|
||||
|
||||
### Overwriting Materialious defaults
|
||||
@@ -269,3 +272,59 @@ VITE_DEFAULT_PEERJS_HOST: "peerjs.example.com"
|
||||
VITE_DEFAULT_PEERJS_PATH: "/"
|
||||
VITE_DEFAULT_PEERJS_PORT: 443
|
||||
```
|
||||
|
||||
|
||||
## Step 7 (Optional): Enabling downloads
|
||||
|
||||
### Step 1: Add the following to your reverse proxy
|
||||
|
||||
#### Caddy
|
||||
|
||||
```caddy
|
||||
materialious.example.com {
|
||||
reverse_proxy localhost:3001
|
||||
|
||||
header {
|
||||
Cross-Origin-Opener-Policy "same-origin"
|
||||
Cross-Origin-Embedder-Policy "require-corp"
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name materialious.example.com;
|
||||
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3001;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Traefik
|
||||
Add the following to Materialious
|
||||
|
||||
```yaml
|
||||
http:
|
||||
middlewares:
|
||||
materialious-downloads:
|
||||
headers:
|
||||
customResponseHeaders:
|
||||
Cross-Origin-Opener-Policy: "same-origin"
|
||||
Cross-Origin-Embedder-Policy: "require-corp"
|
||||
```
|
||||
|
||||
|
||||
### Step 2: Modify docker compose.
|
||||
|
||||
Add this environment variable to Materialious.
|
||||
```yaml
|
||||
VITE_DEFAULT_DOWNLOAD_ENABLED: true
|
||||
```
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ RUN echo "VITE_DEFAULT_INVIDIOUS_INSTANCE=VITE_DEFAULT_INVIDIOUS_INSTANCE_PLACEH
|
||||
echo "VITE_DEFAULT_PEERJS_HOST=VITE_DEFAULT_PEERJS_HOST_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_PEERJS_PATH=VITE_DEFAULT_PEERJS_PATH_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_PEERJS_PORT=VITE_DEFAULT_PEERJS_PORT_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_DOWNLOAD_ENABLED=VITE_DEFAULT_DOWNLOAD_ENABLED_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_SYNCIOUS_INSTANCE=VITE_DEFAULT_SYNCIOUS_INSTANCE_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_SETTINGS='\"VITE_DEFAULT_SETTINGS_PLACEHOLDER\"'" >> .env && \
|
||||
echo "VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE=VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE_PLACEHOLDER" >> .env
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 28
|
||||
versionName "1.2.3"
|
||||
versionCode 29
|
||||
versionName "1.3.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
Generated
+263
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.3",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor/android": "6.1.2",
|
||||
@@ -32,6 +32,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@sveltejs/adapter-auto": "^3.2.4",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.26",
|
||||
@@ -2607,6 +2609,36 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ffmpeg/ffmpeg": {
|
||||
"version": "0.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.10.tgz",
|
||||
"integrity": "sha512-lVtk8PW8e+NUzGZhPTWj2P1J4/NyuCrbDD3O9IGpSeLYtUZKBqZO8CNj1WYGghep/MXoM8e1qVY1GztTkf8YYQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ffmpeg/types": "^0.12.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ffmpeg/types": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg/types/-/types-0.12.2.tgz",
|
||||
"integrity": "sha512-NJtxwPoLb60/z1Klv0ueshguWQ/7mNm106qdHkB4HL49LXszjhjCCiL+ldHJGQ9ai2Igx0s4F24ghigy//ERdA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ffmpeg/util": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg/util/-/util-0.12.1.tgz",
|
||||
"integrity": "sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz",
|
||||
@@ -3477,6 +3509,235 @@
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.24.tgz",
|
||||
"integrity": "sha512-FzJaai6z6DYdICAY1UKNN5pzTn296ksK2zzEjjaXlpZtoMkGktWT0ttS7hbdBCPGhLOu5Q9TA2zdPejKUFjgig==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.7.24",
|
||||
"@swc/core-darwin-x64": "1.7.24",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.7.24",
|
||||
"@swc/core-linux-arm64-gnu": "1.7.24",
|
||||
"@swc/core-linux-arm64-musl": "1.7.24",
|
||||
"@swc/core-linux-x64-gnu": "1.7.24",
|
||||
"@swc/core-linux-x64-musl": "1.7.24",
|
||||
"@swc/core-win32-arm64-msvc": "1.7.24",
|
||||
"@swc/core-win32-ia32-msvc": "1.7.24",
|
||||
"@swc/core-win32-x64-msvc": "1.7.24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.24.tgz",
|
||||
"integrity": "sha512-s0k09qAcsoa8jIncwgRRd43VApYqXu28R4OmICtDffV4S01HtsRLRarXsMuLutoZk3tbxqitep+A8MPBuqNgdg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.24.tgz",
|
||||
"integrity": "sha512-1dlsulJ/fiOoJoJyQgaCewIEaZ7Sh6aJN4r5Uhl4lIZuNWa27XOb28A3K29/6HDO9JML3IJrvXPnl5o0vxDQuQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.24.tgz",
|
||||
"integrity": "sha512-2ft1NmxyvHCu5CY4r2rNVybPqZtJaxpRSzvCcPlVjN/2D5Q3QgM5kBoo1t+0RCFfk4TS2V0KWJhtqKz0CNX62Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.24.tgz",
|
||||
"integrity": "sha512-v/Z8I9tUUNkNHKa1Sw4r1Q7Wp66ezbRhe6xMIxvPNKVJQFaMOsRpe0t8T5qbk5sV2hJGOCKpQynSpZqQXLcJDQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.24.tgz",
|
||||
"integrity": "sha512-0jJx0IcajcyOXaJsx1jXy86lYVrbupyy2VUj/OiJux/ic4oBJLjfL+WOuc8T8/hZj2p6X0X4jvfSCqWSuic4kA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.24.tgz",
|
||||
"integrity": "sha512-2+3aKQpSGjVnWKDTKUPuJzitQlTQrGorg+PVFMRkv6l+RcNCHZQNe/8VYpMhyBhxDMb3LUlbp7776FRevcruxg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.24.tgz",
|
||||
"integrity": "sha512-PMQ6SkCtMoj0Ks77DiishpEmIuHpYjFLDuVOzzJCzGeGoii0yRP5lKy/VeglFYLPqJzmhK9BHlpVehVf/8ZpvA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.24.tgz",
|
||||
"integrity": "sha512-SNdCa4DtGXNWrPVHqctVUxgEVZVETuqERpqF50KFHO0Bvf5V/m1IJ4hFr2BxXlrzgnIW4t1Dpi6YOJbcGbEmnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.24.tgz",
|
||||
"integrity": "sha512-5p3olHqwibMfrVFg2yVuSIPh9HArDYYlJXNZ9JKqeZk23A19J1pl9MuPmXDw+sxsiPfYJ/nUedIGeUHPF/+EDw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.7.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.24.tgz",
|
||||
"integrity": "sha512-gRyPIxDznS8d2ClfmWbytjp2d48bij6swHnDLWhukNuOvXdQkEmaIzjEsionFG/zhcFLnz8zKfTvjEjInAMzxg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz",
|
||||
"integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@trapezedev/gradle-parse": {
|
||||
"version": "7.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@trapezedev/gradle-parse/-/gradle-parse-7.0.10.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -13,6 +13,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@sveltejs/adapter-auto": "^3.2.4",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.26",
|
||||
|
||||
@@ -10,5 +10,6 @@ find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VI
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_HOST_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_HOST"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_PATH_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_PATH"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_PORT_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_PORT"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_DOWNLOAD_ENABLED_PLACEHOLDER|'"$VITE_DEFAULT_DOWNLOAD_ENABLED"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SYNCIOUS_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_SYNCIOUS_INSTANCE"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SETTINGS_PLACEHOLDER|'"$VITE_DEFAULT_SETTINGS"'|g' {} +
|
||||
|
||||
@@ -6,30 +6,26 @@
|
||||
|
||||
export let channel: Channel;
|
||||
|
||||
let loading = true;
|
||||
let channelPfp: string;
|
||||
|
||||
let img: HTMLImageElement;
|
||||
|
||||
onMount(() => {
|
||||
img = new Image();
|
||||
img.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails)) || '';
|
||||
|
||||
img.onload = () => {
|
||||
loading = false;
|
||||
};
|
||||
img.onerror = () => {
|
||||
loading = false;
|
||||
};
|
||||
onMount(async () => {
|
||||
const pfpResp = await fetch(proxyGoogleImage(getBestThumbnail(channel.authorThumbnails)));
|
||||
channelPfp = URL.createObjectURL(await pfpResp.blob());
|
||||
});
|
||||
</script>
|
||||
|
||||
<a href={`/channel/${channel.authorId}`} class="wave" style="min-width: 100%;min-height: 100%;">
|
||||
<div class="padding">
|
||||
<div class="center-align">
|
||||
{#if loading}
|
||||
{#if !channelPfp}
|
||||
<progress class="circle"></progress>
|
||||
{:else}
|
||||
<img class="circle" style="width: 90px;height: 90px;" src={img.src} alt={channel.author} />
|
||||
<img
|
||||
class="circle"
|
||||
style="width: 90px;height: 90px;"
|
||||
src={channelPfp}
|
||||
alt={channel.author}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<h5 class="center-align">{truncate(channel.author, 14)}</h5>
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
let replies: Comments | undefined = undefined;
|
||||
|
||||
async function loadPfp(url: string): Promise<string> {
|
||||
const resp = await fetch(url);
|
||||
return URL.createObjectURL(await resp.blob());
|
||||
}
|
||||
|
||||
async function loadReplies(continuation: string) {
|
||||
try {
|
||||
replies = await getComments(videoId, {
|
||||
@@ -31,11 +36,9 @@
|
||||
</script>
|
||||
|
||||
<div class="comment">
|
||||
<img
|
||||
class="circle small"
|
||||
src={proxyGoogleImage(getBestThumbnail(comment.authorThumbnails))}
|
||||
alt="comment profile"
|
||||
/>
|
||||
{#await loadPfp(proxyGoogleImage(getBestThumbnail(comment.authorThumbnails))) then pfp}
|
||||
<img class="circle small" src={pfp} alt="comment profile" />
|
||||
{/await}
|
||||
<div>
|
||||
<div class="row">
|
||||
<a href={`/channel/${comment.authorId}`}>
|
||||
@@ -60,12 +63,9 @@
|
||||
<p><i>thumb_up</i> {numberWithCommas(comment.likeCount)}</p>
|
||||
{#if comment.creatorHeart}
|
||||
<div>
|
||||
<img
|
||||
class="circle"
|
||||
style="width: 25px; height: 25px"
|
||||
src={proxyGoogleImage(comment.creatorHeart.creatorThumbnail)}
|
||||
alt="Creator profile"
|
||||
/>
|
||||
{#await loadPfp(proxyGoogleImage(comment.creatorHeart.creatorThumbnail)) then pfp}
|
||||
<img class="circle" style="width: 25px; height: 25px" src={pfp} alt="Creator profile" />
|
||||
{/await}
|
||||
<i style="font-size: 20px;margin-left: 5px;" class="absolute left red-text bottom fill"
|
||||
>favorite</i
|
||||
>
|
||||
|
||||
@@ -85,13 +85,8 @@
|
||||
|
||||
async function loadAuthor() {
|
||||
const channel = await getChannel(video.authorId);
|
||||
|
||||
const authorImgObject = new Image();
|
||||
authorImgObject.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails));
|
||||
|
||||
authorImgObject.onload = () => {
|
||||
authorImg = authorImgObject.src;
|
||||
};
|
||||
const imgResp = await fetch(proxyGoogleImage(getBestThumbnail(channel.authorThumbnails)));
|
||||
authorImg = URL.createObjectURL(await imgResp.blob());
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
"youtubeLink": "Copy Youtube link"
|
||||
},
|
||||
"download": "Download",
|
||||
"downloadSteps": {
|
||||
"merging": "Merging video & audio together",
|
||||
"video": "Downloading video",
|
||||
"audio": "Downloading audio"
|
||||
},
|
||||
"noPlaylists": "No playlists",
|
||||
"unableToLoadComments": "Unable to load comments",
|
||||
"addToPlaylist": "Add to playlist"
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { proxyVideoUrl } from './misc';
|
||||
|
||||
interface MediaQuality {
|
||||
bandwidth: string;
|
||||
videoUrl: string;
|
||||
audioUrl: string;
|
||||
resolution?: string;
|
||||
frameRate?: string;
|
||||
audioChannels?: string;
|
||||
audioBandwidth?: string;
|
||||
}
|
||||
|
||||
// https://github.com/ffmpegwasm/ffmpeg.wasm/issues/603#issuecomment-1982289892
|
||||
// Ripped from https://github.com/ffmpegwasm/ffmpeg.wasm/blob/ae1cdac7db79c5315f9a1b716fcbd9bcfe27c902/packages/util/src/index.ts#L49
|
||||
export async function fetchFile(
|
||||
file: string,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Uint8Array> {
|
||||
const response = await fetch(file);
|
||||
|
||||
const contentLength = response.headers.get('content-length');
|
||||
|
||||
// If content-length is missing, download without progress tracking
|
||||
if (!contentLength) {
|
||||
const data = await response.arrayBuffer();
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
|
||||
const totalBytes = parseInt(contentLength, 10);
|
||||
const data = new Uint8Array(totalBytes);
|
||||
let position = 0;
|
||||
let downloadBytes = 0;
|
||||
|
||||
const reader = response.body!.getReader();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
data.set(value, position);
|
||||
position += value.length;
|
||||
downloadBytes += value.byteLength;
|
||||
|
||||
if (onProgress) {
|
||||
onProgress((downloadBytes / totalBytes) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function toBlobURL(
|
||||
url: string,
|
||||
mimeType: string,
|
||||
): Promise<string> {
|
||||
const response = await fetch(url);
|
||||
return URL.createObjectURL(new Blob([await response.arrayBuffer()], { type: mimeType }));
|
||||
}
|
||||
|
||||
|
||||
// Fetch and parse the DASH manifest
|
||||
async function fetchDASHManifest(manifestUrl: string) {
|
||||
const response = await fetch(manifestUrl);
|
||||
const manifestText = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const manifest = parser.parseFromString(manifestText, "text/xml");
|
||||
return manifest;
|
||||
}
|
||||
|
||||
|
||||
export async function listCombinedQualities(manifestUrl: string): Promise<MediaQuality[]> {
|
||||
const manifest = await fetchDASHManifest(manifestUrl);
|
||||
const qualities: MediaQuality[] = [];
|
||||
|
||||
// Fetch all AdaptationSets (both video and audio)
|
||||
const adaptationSets = manifest.getElementsByTagName('AdaptationSet');
|
||||
|
||||
let videoRepresentations: Element[] = [];
|
||||
let audioRepresentations: Element[] = [];
|
||||
|
||||
// Separate video and audio AdaptationSets based on mimeType
|
||||
for (let i = 0; i < adaptationSets.length; i++) {
|
||||
const mimeType = adaptationSets[i].getAttribute('mimeType');
|
||||
const representations = adaptationSets[i].getElementsByTagName('Representation');
|
||||
|
||||
if (mimeType && mimeType === 'video/mp4') {
|
||||
videoRepresentations = [...videoRepresentations, ...Array.from(representations)];
|
||||
} else if (mimeType && mimeType === 'audio/mp4') {
|
||||
audioRepresentations = [...audioRepresentations, ...Array.from(representations)];
|
||||
}
|
||||
}
|
||||
|
||||
// Combine video and audio representations
|
||||
videoRepresentations.forEach((videoRep) => {
|
||||
const videoBandwidth = videoRep.getAttribute('bandwidth');
|
||||
const videoUrl = proxyVideoUrl(videoRep.getElementsByTagName('BaseURL')[0].textContent as string);
|
||||
const width = videoRep.getAttribute('width');
|
||||
const height = videoRep.getAttribute('height');
|
||||
const resolution = width && height ? `${width}x${height}` : undefined;
|
||||
const frameRate = videoRep.getAttribute('frameRate') || '';
|
||||
|
||||
// Pair this video representation with each audio representation
|
||||
audioRepresentations.forEach((audioRep) => {
|
||||
const audioUrl = proxyVideoUrl(audioRep.getElementsByTagName('BaseURL')[0].textContent as string);
|
||||
const audioChannels = audioRep.getElementsByTagName('AudioChannelConfiguration')[0]?.getAttribute('value') || '';
|
||||
const audioBandwidth = audioRep.getAttribute('bandwidth') || '';
|
||||
|
||||
qualities.push({
|
||||
bandwidth: videoBandwidth || '',
|
||||
videoUrl,
|
||||
audioUrl,
|
||||
resolution,
|
||||
frameRate,
|
||||
audioChannels,
|
||||
audioBandwidth
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return qualities;
|
||||
}
|
||||
|
||||
|
||||
export async function mergeMediaFromDASH(
|
||||
selectedQuality: MediaQuality,
|
||||
fileName: string,
|
||||
progressCallbacks?: {
|
||||
video?: (progress: number) => void,
|
||||
audio?: (progress: number) => void,
|
||||
merging?: (progress: number) => void,
|
||||
}
|
||||
) {
|
||||
const ffmpeg = new FFmpeg();
|
||||
|
||||
ffmpeg.on('progress', ({ progress }) => {
|
||||
if (progressCallbacks?.merging) {
|
||||
progressCallbacks.merging(progress * 100);
|
||||
}
|
||||
});
|
||||
|
||||
await ffmpeg.load({
|
||||
coreURL: await toBlobURL('/ffmpeg/ffmpeg-core.js', 'text/javascript'),
|
||||
wasmURL: await toBlobURL('/ffmpeg/ffmpeg-core.wasm', 'application/wasm'),
|
||||
workerURL: await toBlobURL('/ffmpeg/ffmpeg-core.worker.js', 'text/javascript')
|
||||
});
|
||||
|
||||
const { videoUrl, audioUrl } = selectedQuality;
|
||||
|
||||
ffmpeg.writeFile('input.mp4', await fetchFile(videoUrl, progressCallbacks?.video));
|
||||
ffmpeg.writeFile('input.mp3', await fetchFile(audioUrl, progressCallbacks?.audio));
|
||||
|
||||
await ffmpeg.exec(['-i', 'input.mp4', '-i', 'input.mp3', '-c:v', 'copy', '-c:a', 'aac', 'output.mp4']);
|
||||
|
||||
const data = await ffmpeg.readFile('output.mp4');
|
||||
const mergedFile = new Blob([data], { type: 'video/mp4' });
|
||||
const url = URL.createObjectURL(mergedFile);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}.mp4`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
@@ -58,9 +58,15 @@
|
||||
displayContent = await getChannelContent(data.channel.authorId, { type: tab });
|
||||
}
|
||||
|
||||
let channelPfp: string;
|
||||
onMount(async () => {
|
||||
displayContent = await getChannelContent(data.channel.authorId, { type: 'videos' });
|
||||
|
||||
const channelPfpResp = await fetch(
|
||||
proxyGoogleImage(getBestThumbnail(data.channel.authorThumbnails))
|
||||
);
|
||||
channelPfp = URL.createObjectURL(await channelPfpResp.blob());
|
||||
|
||||
if (get(authStore)) {
|
||||
isSubscribed = await amSubscribed(data.channel.authorId);
|
||||
}
|
||||
@@ -86,12 +92,16 @@
|
||||
/>
|
||||
{/if}
|
||||
<div class="description">
|
||||
<img
|
||||
style="margin-right: 1em;"
|
||||
class="circle extra m l"
|
||||
src={proxyGoogleImage(getBestThumbnail(data.channel.authorThumbnails))}
|
||||
alt="Channel profile"
|
||||
/>
|
||||
{#if channelPfp}
|
||||
<img
|
||||
style="margin-right: 1em;"
|
||||
class="circle extra m l"
|
||||
src={channelPfp}
|
||||
alt="Channel profile"
|
||||
/>
|
||||
{:else}
|
||||
<progress style="padding: 15px;" class="circle"></progress>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<h2>{data.channel.author}</h2>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
syncPartyConnectionsStore,
|
||||
syncPartyPeerStore
|
||||
} from '$lib/store';
|
||||
import { mergeMediaFromDASH } from '$lib/videoDownload';
|
||||
import type { DataConnection } from 'peerjs';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
@@ -391,13 +392,26 @@
|
||||
theatreMode = !theatreMode;
|
||||
}
|
||||
|
||||
function downloadFile(url: string, container: string | undefined) {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = container || 'mp3';
|
||||
anchor.target = '_blank';
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
let downloadStage: string | undefined;
|
||||
let downloadProgress: number = 0;
|
||||
|
||||
function onVideoDownloadProgress(progress: number) {
|
||||
downloadStage = $_('player.downloadSteps.video');
|
||||
downloadProgress = progress;
|
||||
}
|
||||
|
||||
function onAudioDownloadProgress(progress: number) {
|
||||
downloadStage = $_('player.downloadSteps.audio');
|
||||
downloadProgress = progress;
|
||||
}
|
||||
|
||||
function onMergingProgress(progress: number) {
|
||||
downloadStage = $_('player.downloadSteps.merging');
|
||||
downloadProgress = progress;
|
||||
|
||||
if (progress >= 100) {
|
||||
downloadStage = undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -418,6 +432,14 @@
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if downloadStage}
|
||||
<article>
|
||||
<h6>{downloadStage} ({Math.round(downloadProgress)}%)</h6>
|
||||
<progress class="max" value={downloadProgress} max="100"></progress>
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
<h5>{letterCase(data.video.title)}</h5>
|
||||
|
||||
<div class="grid no-padding">
|
||||
@@ -506,22 +528,28 @@
|
||||
<ShareVideo video={data.video} />
|
||||
</menu></button
|
||||
>
|
||||
{#if data.downloadOptions.length > 0}
|
||||
<button class="border"
|
||||
><i>download</i>
|
||||
<div class="tooltip">{$_('player.download')}</div>
|
||||
<menu class="no-wrap">
|
||||
{#each data.downloadOptions as download}
|
||||
<a
|
||||
class="row"
|
||||
href="#download"
|
||||
on:click={() => downloadFile(download.url, download.container)}
|
||||
>{download.title}</a
|
||||
>
|
||||
{/each}
|
||||
</menu></button
|
||||
>
|
||||
{/if}
|
||||
{#await data.streamed.downloadQualitiesDash then downloadQualitiesDash}
|
||||
{#if downloadQualitiesDash}
|
||||
<button class="border"
|
||||
><i>download</i>
|
||||
<div class="tooltip">{$_('player.download')}</div>
|
||||
<menu class="no-wrap">
|
||||
{#each downloadQualitiesDash as quality}
|
||||
<a
|
||||
class="row"
|
||||
href="#download"
|
||||
on:click={async () =>
|
||||
await mergeMediaFromDASH(quality, data.video.title, {
|
||||
video: onVideoDownloadProgress,
|
||||
audio: onAudioDownloadProgress,
|
||||
merging: onMergingProgress
|
||||
})}>{quality.resolution}</a
|
||||
>
|
||||
{/each}
|
||||
</menu></button
|
||||
>
|
||||
{/if}
|
||||
{/await}
|
||||
{#if personalPlaylists}
|
||||
<button class="border">
|
||||
<i>add</i>
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
import { phaseDescription } from '$lib/misc';
|
||||
import {
|
||||
authStore,
|
||||
playerDashStore,
|
||||
playerProxyVideosStore,
|
||||
returnYTDislikesInstanceStore,
|
||||
returnYtDislikesStore
|
||||
} from '$lib/store';
|
||||
import { listCombinedQualities } from '$lib/videoDownload';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
@@ -24,26 +26,6 @@ export async function load({ params, url }) {
|
||||
error(500, errorMessage);
|
||||
}
|
||||
|
||||
let downloadOptions: { title: string; url: string; container: string | undefined; }[] = [];
|
||||
|
||||
if (!video.hlsUrl) {
|
||||
video.formatStreams.forEach((format) => {
|
||||
downloadOptions.push({
|
||||
title: `${format.type.split(';')[0].trim()} - ${format.qualityLabel} (With audio)`,
|
||||
url: format.url,
|
||||
container: format.container
|
||||
});
|
||||
});
|
||||
|
||||
video.adaptiveFormats.forEach((format) => {
|
||||
downloadOptions.push({
|
||||
title: `${format.type.split(';')[0].trim()} - ${format.qualityLabel || format.bitrate + ' bitrate'}`,
|
||||
url: format.url,
|
||||
container: format.container
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let personalPlaylists;
|
||||
if (get(authStore)) {
|
||||
postHistory(video.videoId);
|
||||
@@ -61,7 +43,7 @@ export async function load({ params, url }) {
|
||||
comments = null;
|
||||
}
|
||||
|
||||
let returnYTDislikes = null;
|
||||
let returnYTDislikes;
|
||||
const returnYTDislikesInstance = get(returnYTDislikesInstanceStore);
|
||||
if (returnYTDislikesInstance && returnYTDislikesInstance !== '') {
|
||||
try {
|
||||
@@ -69,16 +51,25 @@ export async function load({ params, url }) {
|
||||
} catch { }
|
||||
}
|
||||
|
||||
let downloadQualitiesDash;
|
||||
if (
|
||||
typeof import.meta.env.VITE_DEFAULT_DOWNLOAD_ENABLED !== 'undefined' &&
|
||||
import.meta.env.VITE_DEFAULT_DOWNLOAD_ENABLED.toString() === 'true' &&
|
||||
typeof video.hlsUrl === 'undefined' && get(playerDashStore)
|
||||
) {
|
||||
downloadQualitiesDash = listCombinedQualities(video.dashUrl);
|
||||
}
|
||||
|
||||
return {
|
||||
video: video,
|
||||
content: phaseDescription(video.descriptionHtml),
|
||||
playlistId: url.searchParams.get('playlist'),
|
||||
downloadOptions: downloadOptions,
|
||||
streamed: {
|
||||
personalPlaylists: personalPlaylists,
|
||||
returnYTDislikes: returnYTDislikes,
|
||||
comments: comments,
|
||||
subscribed: amSubscribed(video.authorId)
|
||||
subscribed: amSubscribed(video.authorId),
|
||||
downloadQualitiesDash: downloadQualitiesDash
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
"use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",data=>onmessage({data:data}));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f)},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./ffmpeg-core.js")).then(exports=>exports.default(Module))}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;
|
||||
@@ -1,8 +1,10 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { vite as vidstack } from 'vidstack/plugins';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
SvelteKitPWA({
|
||||
@@ -32,5 +34,13 @@ export default defineConfig({
|
||||
}),
|
||||
vidstack(),
|
||||
sveltekit()
|
||||
]
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util']
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
|
||||
LATEST_VERSION = "1.2.3"
|
||||
LATEST_VERSION = "1.3.0"
|
||||
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")
|
||||
|
||||
ROOT_PACKAGE = os.path.join(WORKING_DIR, "package.json")
|
||||
|
||||
Reference in New Issue
Block a user