diff --git a/.github/workflows/docker-build-push.yaml b/.github/workflows/docker-build-push.yaml new file mode 100644 index 0000000..520c9a9 --- /dev/null +++ b/.github/workflows/docker-build-push.yaml @@ -0,0 +1,72 @@ +name: Build and Push Docker Image + +# Define when this workflow will run +on: + push: + branches: + - master # Trigger on pushes to master branch + tags: + - '[0-9]+.[0-9]+.[0-9]+' # Trigger on semantic version tags + paths-ignore: + - 'Cargo.lock' + - 'LICENSE' + - 'README.md' + - 'docker-compose.yml' + workflow_dispatch: # Allow manual triggering of the workflow + +# Define environment variables used throughout the workflow +env: + REGISTRY: quay.io + IMAGE_NAME: invidious/invidious-companion + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + # Step 1: Check out the repository code + - name: Checkout code + uses: actions/checkout@v3 + + # Step 2: Set up QEMU for multi-architecture builds + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + # Step 3: Set up Docker Buildx for enhanced build capabilities + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # Step 4: Authenticate with Quay.io registry + - name: Log in to Quay.io + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + # Step 5: Extract metadata for Docker image tagging and labeling + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # Define tagging strategy + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=sha,prefix={{branch}}- + # Define labels + labels: | + quay.expires-after=12w + + # Step 6: Build and push the Docker image + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 # Build for multiple architectures + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 1ed9a2a..e4aeba3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ COPY --from=builder /app/invidious_companion /app/ COPY ./config/ /app/config/ COPY --from=builder /tini /tini +ENV PORT=8282 \ + HOST=0.0.0.0 + # Copy passwd file for the non-privileged user from the user-stage COPY --from=user-stage /etc/passwd /etc/passwd diff --git a/config/default.toml b/config/default.toml index c1c50b3..92aba3a 100644 --- a/config/default.toml +++ b/config/default.toml @@ -6,6 +6,9 @@ base_url = "http://localhost:8282" [cache] enabled = true +# will get cached in /var/tmp/youtubei.js if you specify /var/tmp +# you need to change the --allow-write from deno run too +directory = "/var/tmp" [networking] #proxy = "" diff --git a/deno.json b/deno.json index e1545b4..cd98fa9 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,7 @@ { "tasks": { - "dev": "deno run --allow-net --allow-env --allow-read --allow-sys=hostname --allow-write=./,/tmp/youtubei.js --watch src/main.ts", - "compile": "deno compile --output invidious_companion --allow-net --allow-env --allow-sys=hostname --allow-read src/main.ts" + "dev": "deno run --allow-net --allow-env --allow-sys=hostname --allow-write=/var/tmp/youtubei.js --watch src/main.ts", + "compile": "deno compile --output invidious_companion --allow-net --allow-env --allow-read --allow-sys=hostname --allow-write=/var/tmp/youtubei.js src/main.ts" }, "imports": { "hono": "jsr:@hono/hono@^4.6.5", @@ -9,9 +9,10 @@ "hono/bearer-auth": "jsr:@hono/hono@^4.6.5/bearer-auth", "youtubei.js": "https://deno.land/x/youtubei@v11.0.1-deno/deno.ts", "youtubei.js/endpoints": "https://deno.land/x/youtubei@v11.0.1-deno/deno/src/core/endpoints/index.ts", + "youtubei.js/Utils": "https://deno.land/x/youtubei@v11.0.1-deno/deno/src/utils/Utils.ts", "jsdom": "https://esm.sh/jsdom@25.0.1", "bgutils": "https://esm.sh/bgutils-js@3.0.0", - "estree": "npm:@types/estree", + "estree": "https://esm.sh/@types/estree@1.0.6", "@willsoto/node-konfig-core": "npm:@willsoto/node-konfig-core@5.0.0", "@willsoto/node-konfig-file": "npm:@willsoto/node-konfig-file@3.0.0", "@willsoto/node-konfig-toml-parser": "npm:@willsoto/node-konfig-toml-parser@3.0.0", diff --git a/deno.lock b/deno.lock index ef4059e..3b11d0b 100644 --- a/deno.lock +++ b/deno.lock @@ -740,7 +740,6 @@ "workspace": { "dependencies": [ "jsr:@hono/hono@^4.6.5", - "npm:@types/estree", "npm:@willsoto/node-konfig-core@5.0.0", "npm:@willsoto/node-konfig-file@3.0.0", "npm:@willsoto/node-konfig-toml-parser@3.0.0" diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..36cdb77 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,19 @@ +version: "3" +services: + invidious_companion: + build: + context: . + dockerfile: Dockerfile + # image: quay.io/invidious/invidious-companion:latest + ports: + - 127.0.0.1:8282:8282 + restart: unless-stopped + cap_drop: + - ALL + read_only: true + user: 10001:10001 + # cache for youtube library + volumes: + - /var/tmp/youtubei.js:/var/tmp/youtubei.js:rw + security_opt: + - no-new-privileges:true diff --git a/src/lib/helpers/getFetchClient.ts b/src/lib/helpers/getFetchClient.ts index ae641b5..2d3bdc6 100644 --- a/src/lib/helpers/getFetchClient.ts +++ b/src/lib/helpers/getFetchClient.ts @@ -6,14 +6,14 @@ export const getFetchClient = (konfigStore: Store): { client: Deno.HttpClient; }): Promise; } => { - if (konfigStore.get("networking.proxy")) { + if (Deno.env.get("PROXY") || konfigStore.get("networking.proxy")) { return async ( input: RequestInfo | URL, init?: RequestInit, ) => { const client = Deno.createHttpClient({ proxy: { - url: konfigStore.get("networking.proxy") as string, + url: Deno.env.get("PROXY") || konfigStore.get("networking.proxy") as string, }, }); const fetchRes = await fetch(input, { diff --git a/src/lib/helpers/youtubePlayerHandling.ts b/src/lib/helpers/youtubePlayerHandling.ts index b157f2d..1daaea5 100644 --- a/src/lib/helpers/youtubePlayerHandling.ts +++ b/src/lib/helpers/youtubePlayerHandling.ts @@ -1,4 +1,5 @@ import { Innertube, YT, ApiResponse } from "youtubei.js"; +import { generateRandomString } from "youtubei.js/Utils"; import { compress, decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts"; import { youtubePlayerReq } from "youtubePlayerReq"; import { Store } from "@willsoto/node-konfig-core"; @@ -28,7 +29,7 @@ export const youtubePlayerParsing = async ( const video = new YT.VideoInfo( [youtubePlayerResponse], innertubeClient.actions, - "", + generateRandomString(16), ); const streamingData = video.streaming_data; @@ -89,7 +90,7 @@ export const youtubePlayerParsing = async ( videoDetails, microformat, invidiousCompanion: { - "baseUrl": konfigStore.get("server.base_url") as string, + "baseUrl": Deno.env.get("SERVER_BASE_URL") || konfigStore.get("server.base_url") as string, }, }))(videoData); diff --git a/src/lib/jobs/potoken.ts b/src/lib/jobs/potoken.ts index aef0090..6bd6916 100644 --- a/src/lib/jobs/potoken.ts +++ b/src/lib/jobs/potoken.ts @@ -9,6 +9,7 @@ import { getFetchClient } from "../helpers/getFetchClient.ts"; export const poTokenGenerate = async ( innertubeClient: Innertube, konfigStore: Store>, + innertubeClientCache: UniversalCache ): Promise => { const requestKey = "O43z0dpjhgX20SCx4KAo"; @@ -62,7 +63,7 @@ export const poTokenGenerate = async ( po_token: poTokenResult.poToken, visitor_data: visitorData, fetch: getFetchClient(konfigStore), - cache: new UniversalCache(true), + cache: innertubeClientCache, generate_session_locally: true, })); }; diff --git a/src/main.ts b/src/main.ts index 18ae305..06c54f9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,11 +6,9 @@ import { konfigLoader } from "./lib/helpers/konfigLoader.ts"; import { getFetchClient } from "./lib/helpers/getFetchClient.ts"; const app = new Hono(); - const konfigStore = await konfigLoader(); let innertubeClient: Innertube; -let innertubeClientUniversalCache = true; let innertubeClientFetchPlayer = true; const innertubeClientOauthEnabled = konfigStore.get( "youtube_session.oauth_enabled", @@ -21,6 +19,12 @@ const innertubeClientJobPoTokenEnabled = konfigStore.get( const innertubeClientCookies = konfigStore.get( "jobs.youtube_session.cookies", ) as string; +let innertubeClientCache = new UniversalCache( + true, + konfigStore.get('cache.directory') as string + "/youtubei.js/", +) as UniversalCache; + +Deno.env.set('TMPDIR', konfigStore.get("cache.directory") as string) if (!innertubeClientOauthEnabled) { if (innertubeClientJobPoTokenEnabled) { @@ -32,29 +36,37 @@ if (!innertubeClientOauthEnabled) { } } else if (innertubeClientOauthEnabled) { // Can't use cache if using OAuth#cacheCredentials - innertubeClientUniversalCache = false; + innertubeClientCache = new UniversalCache(false); } innertubeClient = await Innertube.create({ - cache: new UniversalCache(innertubeClientUniversalCache), + cache: innertubeClientCache, retrieve_player: innertubeClientFetchPlayer, fetch: getFetchClient(konfigStore), - cookie: innertubeClientCookies || undefined + cookie: innertubeClientCookies || undefined, }); if (!innertubeClientOauthEnabled) { if (innertubeClientOauthEnabled) { - innertubeClient = await poTokenGenerate(innertubeClient, konfigStore); + innertubeClient = await poTokenGenerate( + innertubeClient, + konfigStore, + innertubeClientCache as UniversalCache, + ); } Deno.cron( "regenerate youtube session", konfigStore.get("jobs.youtube_session.frequency") as string, async () => { if (innertubeClientOauthEnabled) { - innertubeClient = await poTokenGenerate(innertubeClient, konfigStore); + innertubeClient = await poTokenGenerate( + innertubeClient, + konfigStore, + innertubeClientCache, + ); } else { innertubeClient = await Innertube.create({ - cache: new UniversalCache(innertubeClientUniversalCache), + cache: innertubeClientCache, retrieve_player: innertubeClientFetchPlayer, }); } @@ -62,8 +74,10 @@ if (!innertubeClientOauthEnabled) { ); } else if (innertubeClientOauthEnabled) { // Fired when waiting for the user to authorize the sign in attempt. - innertubeClient.session.on('auth-pending', (data) => { - console.log(`Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`); + innertubeClient.session.on("auth-pending", (data) => { + console.log( + `Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`, + ); }); // Fired when authentication is successful. innertubeClient.session.on("auth", () => { @@ -91,6 +105,6 @@ app.use("*", async (c, next) => { routes(app, konfigStore); Deno.serve({ - port: konfigStore.get("server.port") as number, - hostname: konfigStore.get("server.host") as string, + port: Number(Deno.env.get("PORT")) || konfigStore.get("server.port") as number, + hostname: Deno.env.get("HOST") || konfigStore.get("server.host") as string, }, app.fetch); diff --git a/src/routes/index.ts b/src/routes/index.ts index d191881..624b594 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -13,7 +13,7 @@ export const routes = (app: Hono, konfigStore: Store>) = app.use( "/youtubei/v1/*", bearerAuth({ - token: konfigStore.get("server.hmac_key") as string, + token: Deno.env.get("SERVER_HMAC_KEY") || konfigStore.get("server.hmac_key") as string, }), );