├── .dockerignore
├── .env.example
├── .github
└── workflows
│ ├── cleanup-images.yml
│ ├── deploy.yml
│ └── docker-build.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── Dockerfile
├── GITHUB_ACTIONS_README.md
├── README.md
├── build-docker.bat
├── build-docker.sh
├── docker-compose.yml
├── drizzle.config.js
├── drizzle
├── 0000_square_sabretooth.sql
├── meta
│ ├── 0000_snapshot.json
│ └── _journal.json
├── relations.ts
└── schema.ts
├── eslint.config.js
├── jsconfig.json
├── package.json
├── pnpm-lock.yaml
├── run-docker.bat
├── run-docker.sh
├── src
├── app.css
├── app.d.ts
├── app.html
├── auth.ts
├── hooks.server.ts
├── lib
│ ├── components
│ │ ├── CountrySlider.svelte
│ │ ├── Footer.svelte
│ │ ├── MediaCard.svelte
│ │ ├── MediaCarouselCard.svelte
│ │ ├── MediaItemCard.svelte
│ │ ├── Navbar.svelte
│ │ └── Videoplayer.svelte
│ ├── index.js
│ ├── schemas
│ │ └── channel.js
│ ├── server
│ │ ├── db.ts
│ │ └── schema.js
│ ├── store.ts
│ ├── utils.ts
│ ├── utils
│ │ ├── countryNames.js
│ │ └── countryNames.ts
│ └── videojs
│ │ ├── plugins
│ │ └── es
│ │ │ ├── hlsjs.js
│ │ │ ├── liveclock.js
│ │ │ ├── morevideo.css
│ │ │ ├── morevideo.js
│ │ │ ├── nuevo-dash.js
│ │ │ ├── nuevo.d.ts
│ │ │ ├── nuevo.js
│ │ │ ├── nuevo.min.js
│ │ │ ├── overlay.js
│ │ │ ├── playlist.js
│ │ │ ├── upnext.css
│ │ │ ├── upnext.js
│ │ │ ├── video.es.js
│ │ │ ├── video.js
│ │ │ ├── video.min.js
│ │ │ ├── videojs.events.js
│ │ │ ├── videojs.hotkeys.js
│ │ │ ├── videojs.offline.js
│ │ │ ├── videojs.thumbnails.js
│ │ │ ├── videojs.ticker.js
│ │ │ ├── videojs.trailer.css
│ │ │ ├── videojs.trailer.js
│ │ │ ├── videojs.transcript.css
│ │ │ └── videojs.transcript.js
│ │ └── skins
│ │ ├── chrome
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── flow
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── gold1
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── gold2
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── jwlike
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── mockup
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── nuevo
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── party
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── pinko
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── roundal
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── shaka
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ ├── slategrey
│ │ ├── symbol-defs.svg
│ │ ├── videojs.css
│ │ └── videojs.min.css
│ │ └── treso
│ │ ├── symbol-defs.svg
│ │ ├── upnext.css
│ │ ├── videojs.css
│ │ └── videojs.min.css
└── routes
│ ├── +layout.svelte
│ ├── +page.svelte
│ ├── 4k
│ └── +page.svelte
│ ├── admin
│ ├── +layout.server.ts
│ ├── +layout.svelte
│ ├── +page.svelte
│ ├── add
│ │ └── +page.svelte
│ ├── channels
│ │ ├── +page.svelte
│ │ ├── [id]
│ │ │ └── edit
│ │ │ │ └── +page.svelte
│ │ └── add
│ │ │ └── +page.svelte
│ ├── edit
│ │ └── [id]
│ │ │ └── +page.svelte
│ ├── login.svelte
│ ├── login
│ │ └── +page.svelte
│ └── media
│ │ └── +page.svelte
│ ├── api
│ ├── channels
│ │ ├── +server.ts
│ │ └── [id]
│ │ │ └── +server.ts
│ ├── episodes
│ │ ├── +server.ts
│ │ └── [id]
│ │ │ └── +server.ts
│ ├── media
│ │ ├── +server.ts
│ │ └── [id]
│ │ │ └── +server.ts
│ ├── movie-files
│ │ ├── +server.ts
│ │ └── [id]
│ │ │ └── +server.ts
│ └── seed
│ │ └── +server.js
│ ├── details
│ └── [id]
│ │ └── +page.svelte
│ ├── login
│ └── +page.svelte
│ ├── movies
│ └── +page.svelte
│ ├── player
│ └── [id]
│ │ └── +page.svelte
│ ├── series
│ └── +page.svelte
│ └── youth
│ └── +page.svelte
├── static
└── favicon.svg
├── svelte.config.js
├── vite.config.js
└── worker-configuration.d.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | .git
4 | .gitignore
5 | .gitattributes
6 | README.md
7 | .npmrc
8 | .prettierrc
9 | .prettierignore
10 | .eslintrc.cjs
11 | eslint.config.js
12 | .graphqlrc
13 | .editorconfig
14 | .svelte-kit
15 | .vscode
16 | node_modules
17 | build
18 | package
19 | .output
20 | dist
21 |
22 | # Environment files (excluded - will be handled by build script)
23 | **/.env
24 | .env.*
25 | !.env
26 |
27 | # Logs
28 | *.log
29 | npm-debug.log*
30 | pnpm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 |
34 | # Runtime data
35 | pids
36 | *.pid
37 | *.seed
38 | *.pid.lock
39 |
40 | # Coverage directory used by tools like istanbul
41 | coverage
42 | .nyc_output
43 |
44 | # Cache
45 | .cache
46 | .parcel-cache
47 | .next
48 | .nuxt
49 | .vuepress/dist
50 |
51 | # Serverless directories
52 | .serverless
53 | .FuseBox
54 | .dynamodb
55 |
56 | # IDE
57 | .tern-project
58 | .tern-port
59 | .tmp
60 | .temp
61 |
62 | # OS
63 | .DS_Store
64 | Thumbs.db
65 |
66 | # Old site
67 | oldsite
68 |
69 | # Docker and build scripts
70 | build-docker.sh
71 | build-docker.bat
72 | run-docker.sh
73 | run-docker.bat
74 | docker-compose.yml
75 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Production database credentials
2 | DATABASE_URL="libsql://your-production-database-url"
3 | DATABASE_AUTH_TOKEN="your-production-auth-token"
4 |
5 | # Development database credentials (will be used when NODE_ENV=development or when running in dev mode)
6 | DATABASE_URL_DEV="libsql://your-development-database-url"
7 | DATABASE_AUTH_TOKEN_DEV="your-development-auth-token"
8 |
9 | # Environment setting
10 | NODE_ENV="development"
11 |
12 | # Authentication
13 | PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key
14 | CLERK_SECRET_KEY=your_clerk_secret_key
15 |
16 | AUTH_SECRET="your-auth-secret"
17 | AUTH0_CLIENT_ID="your-auth0-client-id"
18 | AUTH0_CLIENT_SECRET="your-auth0-client-secret"
19 | AUTH0_ISSUER_BASE_URL="your-auth0-issuer-url"
20 |
--------------------------------------------------------------------------------
/.github/workflows/cleanup-images.yml:
--------------------------------------------------------------------------------
1 | name: Cleanup Container Images
2 |
3 | on:
4 | push:
5 | workflow_dispatch:
6 |
7 | concurrency:
8 | group: cleanup-${{ github.ref }}
9 | cancel-in-progress: true
10 |
11 | env:
12 | REGISTRY: ghcr.io
13 | IMAGE_NAME: ${{ github.repository }}
14 |
15 | jobs:
16 | cleanup:
17 | runs-on: ubuntu-latest
18 | permissions:
19 | contents: read
20 | packages: write
21 |
22 | steps:
23 | - name: Cleanup old container images
24 | run: |
25 | echo "🧹 Starting comprehensive image cleanup..."
26 | echo "📦 Repository: ${{ env.IMAGE_NAME }}"
27 |
28 | # Extract just the repository name (remove owner/)
29 | REPO_NAME="${{ env.IMAGE_NAME }}"
30 | PACKAGE_NAME="${REPO_NAME##*/}"
31 |
32 | echo "🔍 Getting all package versions..."
33 |
34 | # Get all package versions, including untagged ones
35 | gh api --paginate \
36 | -H "Accept: application/vnd.github+json" \
37 | -H "X-GitHub-Api-Version: 2022-11-28" \
38 | "/user/packages/container/${PACKAGE_NAME}/versions" \
39 | --jq '.[] | {
40 | id: .id,
41 | tags: .metadata.container.tags,
42 | created_at: .created_at,
43 | is_untagged: (.metadata.container.tags | length == 0)
44 | }' > all_versions.json
45 |
46 | # Separate tagged and untagged versions
47 | echo "📋 Analyzing versions..."
48 |
49 | # Get tagged versions (excluding latest and production)
50 | cat all_versions.json | jq -r 'select(.is_untagged == false and (.tags | contains(["latest", "production"]) | not))' | jq -s 'sort_by(.created_at) | reverse' > tagged_versions.json
51 |
52 | # Get untagged versions
53 | cat all_versions.json | jq -r 'select(.is_untagged == true)' | jq -s 'sort_by(.created_at) | reverse' > untagged_versions.json
54 |
55 | # Keep only the 3 most recent tagged versions
56 | TAGGED_COUNT=$(cat tagged_versions.json | jq 'length')
57 | UNTAGGED_COUNT=$(cat untagged_versions.json | jq 'length')
58 |
59 | echo "📊 Found $TAGGED_COUNT tagged versions and $UNTAGGED_COUNT untagged versions"
60 |
61 | # Delete old tagged versions (keep 3 most recent)
62 | if [ "$TAGGED_COUNT" -gt 3 ]; then
63 | TAGGED_TO_DELETE=$(cat tagged_versions.json | jq -r '.[3:] | .[].id')
64 | TAGGED_DELETE_COUNT=$((TAGGED_COUNT - 3))
65 | echo "🗑️ Deleting $TAGGED_DELETE_COUNT old tagged versions:"
66 |
67 | echo "$TAGGED_TO_DELETE" | while read -r version_id; do
68 | if [ -n "$version_id" ]; then
69 | TAGS=$(cat tagged_versions.json | jq -r --arg id "$version_id" '.[] | select(.id == $id) | .tags | join(", ")')
70 | echo " 🏷️ Deleting tagged version: $version_id (tags: $TAGS)"
71 | gh api --method DELETE \
72 | -H "Accept: application/vnd.github+json" \
73 | -H "X-GitHub-Api-Version: 2022-11-28" \
74 | "/user/packages/container/${PACKAGE_NAME}/versions/$version_id" || echo " ❌ Failed to delete $version_id"
75 | fi
76 | done
77 | else
78 | echo "✅ Tagged versions within limit ($TAGGED_COUNT/3)"
79 | fi
80 |
81 | # Delete ALL untagged versions (they're usually build artifacts)
82 | if [ "$UNTAGGED_COUNT" -gt 0 ]; then
83 | UNTAGGED_TO_DELETE=$(cat untagged_versions.json | jq -r '.[].id')
84 | echo "🗑️ Deleting all $UNTAGGED_COUNT untagged versions:"
85 |
86 | echo "$UNTAGGED_TO_DELETE" | while read -r version_id; do
87 | if [ -n "$version_id" ]; then
88 | echo " 📦 Deleting untagged version: $version_id"
89 | gh api --method DELETE \
90 | -H "Accept: application/vnd.github+json" \
91 | -H "X-GitHub-Api-Version: 2022-11-28" \
92 | "/user/packages/container/${PACKAGE_NAME}/versions/$version_id" || echo " ❌ Failed to delete $version_id"
93 | fi
94 | done
95 | else
96 | echo "✅ No untagged versions found"
97 | fi
98 |
99 | # Summary
100 | echo ""
101 | echo "📋 Cleanup Summary:"
102 | echo " 📦 Package: $PACKAGE_NAME"
103 | echo " 🏷️ Tagged versions: $TAGGED_COUNT (kept 3 most recent)"
104 | echo " 📦 Untagged versions: $UNTAGGED_COUNT (deleted all)"
105 | echo " 🧹 Cleanup completed successfully!"
106 |
107 | # Cleanup temp files
108 | rm -f all_versions.json tagged_versions.json untagged_versions.json
109 | env:
110 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
111 |
112 | - name: Cleanup notification
113 | run: |
114 | echo "✅ Container registry cleanup completed!"
115 | echo "🔄 This workflow runs on every push to keep your registry clean"
116 | echo "🛡️ Protected tags: latest, production"
117 | echo "📝 Retention: 3 most recent tagged versions + 0 untagged versions"
118 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Production
2 |
3 | on:
4 | push:
5 | branches: [ main, master ]
6 | workflow_dispatch:
7 |
8 | concurrency:
9 | group: deploy-${{ github.ref }}
10 | cancel-in-progress: true
11 |
12 | env:
13 | REGISTRY: ghcr.io
14 | IMAGE_NAME: ${{ github.repository }}
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | environment: production
20 | permissions:
21 | contents: read
22 | packages: write
23 | attestations: write
24 | id-token: write
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v4
29 |
30 | - name: Set up Docker Buildx
31 | uses: docker/setup-buildx-action@v3
32 |
33 | - name: Log in to Container Registry
34 | uses: docker/login-action@v3
35 | with:
36 | registry: ${{ env.REGISTRY }}
37 | username: ${{ github.actor }}
38 | password: ${{ secrets.GITHUB_TOKEN }}
39 |
40 | - name: Extract metadata (tags, labels) for Docker
41 | id: meta
42 | uses: docker/metadata-action@v5
43 | with:
44 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
45 | tags: |
46 | type=ref,event=branch
47 | type=ref,event=tag
48 | type=sha,prefix=sha-
49 | type=raw,value=latest,enable={{is_default_branch}}
50 | type=raw,value=production
51 | labels: |
52 | org.opencontainers.image.title=${{ github.repository }}
53 | org.opencontainers.image.description=SvelteKit Media Application
54 | org.opencontainers.image.vendor=Media Community
55 |
56 | - name: Create production environment file
57 | run: |
58 | cat > .env << EOF
59 | # Production Environment Variables
60 | NODE_ENV="production"
61 |
62 | # Production Database
63 | DATABASE_URL="${{ secrets.PROD_DATABASE_URL }}"
64 | DATABASE_AUTH_TOKEN="${{ secrets.PROD_DATABASE_AUTH_TOKEN }}"
65 |
66 | # Authentication (shared between dev/prod for admin access)
67 | PUBLIC_CLERK_PUBLISHABLE_KEY=${{ secrets.PUBLIC_CLERK_PUBLISHABLE_KEY }}
68 | CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY }}
69 |
70 | AUTH_SECRET="${{ secrets.AUTH_SECRET }}"
71 | AUTH0_CLIENT_ID="${{ secrets.AUTH0_CLIENT_ID }}"
72 | AUTH0_CLIENT_SECRET="${{ secrets.AUTH0_CLIENT_SECRET }}"
73 | AUTH0_ISSUER_BASE_URL="${{ secrets.AUTH0_ISSUER_BASE_URL }}"
74 | EOF
75 |
76 | - name: Build and push Docker image
77 | uses: docker/build-push-action@v6
78 | with:
79 | context: .
80 | platforms: linux/amd64
81 | push: true
82 | tags: ${{ steps.meta.outputs.tags }}
83 | labels: ${{ steps.meta.outputs.labels }}
84 | annotations: ${{ steps.meta.outputs.annotations }}
85 | build-args: |
86 | BUILDKIT_INLINE_CACHE=1
87 | GIT_SHA=${{ github.sha }}
88 | cache-from: |
89 | type=gha,scope=prod
90 | type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
91 | cache-to: |
92 | type=gha,mode=max,scope=prod
93 | type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
94 | provenance: mode=max
95 | sbom: true
96 |
97 | - name: Clean up environment file
98 | if: always()
99 | run: rm -f .env
100 |
101 | - name: Build notification
102 | run: |
103 | echo "🚀 Docker image built and pushed successfully!"
104 | echo "📦 Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
105 | echo "🏷️ Tags: ${{ steps.meta.outputs.tags }}"
106 | echo "🏗️ Architecture: amd64"
107 |
108 | deploy:
109 | runs-on: ubuntu-latest
110 | needs: build
111 | environment: production
112 | if: success()
113 | permissions:
114 | contents: read
115 |
116 | steps:
117 | - name: Update container image on Magic Containers
118 | uses: BunnyWay/actions/container-update-image@main
119 | with:
120 | app_id: ${{ secrets.APP_ID }}
121 | api_key: ${{ secrets.BUNNYNET_API_KEY }}
122 | container: app
123 | image_tag: "latest"
124 |
125 | - name: Deploy notification
126 | run: |
127 | echo "✅ Production container updated on Magic Containers with latest tag"
128 |
--------------------------------------------------------------------------------
/.github/workflows/docker-build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy Docker Image
2 |
3 | on:
4 | pull_request:
5 | branches: [ main, master ]
6 | workflow_dispatch:
7 |
8 | concurrency:
9 | group: build-${{ github.ref }}
10 | cancel-in-progress: true
11 |
12 | env:
13 | REGISTRY: ghcr.io
14 | IMAGE_NAME: ${{ github.repository }}
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | permissions:
20 | contents: read
21 | packages: write
22 | attestations: write
23 | id-token: write
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v4
28 |
29 | - name: Set up Docker Buildx
30 | uses: docker/setup-buildx-action@v3
31 |
32 | - name: Log in to Container Registry
33 | uses: docker/login-action@v3
34 | with:
35 | registry: ${{ env.REGISTRY }}
36 | username: ${{ github.actor }}
37 | password: ${{ secrets.GITHUB_TOKEN }}
38 |
39 | - name: Extract metadata (tags, labels) for Docker
40 | id: meta
41 | uses: docker/metadata-action@v5
42 | with:
43 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
44 | tags: |
45 | type=ref,event=branch
46 | type=ref,event=pr
47 | type=sha,prefix=sha-
48 | type=raw,value=latest-preview
49 | type=raw,value=latest-dev
50 | labels: |
51 | org.opencontainers.image.title=${{ github.repository }}
52 | org.opencontainers.image.description=SvelteKit Media Application
53 | org.opencontainers.image.vendor=Media Community
54 |
55 | - name: Create production environment file
56 | run: |
57 | cat > .env << EOF
58 | # Production Environment Variables
59 | NODE_ENV="production"
60 |
61 | # Production Database
62 | DATABASE_URL="${{ secrets.DATABASE_URL }}"
63 | DATABASE_AUTH_TOKEN="${{ secrets.DATABASE_AUTH_TOKEN }}"
64 |
65 | # Authentication
66 | PUBLIC_CLERK_PUBLISHABLE_KEY=${{ secrets.PUBLIC_CLERK_PUBLISHABLE_KEY }}
67 | CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY }}
68 |
69 | AUTH_SECRET="${{ secrets.AUTH_SECRET }}"
70 | AUTH0_CLIENT_ID="${{ secrets.AUTH0_CLIENT_ID }}"
71 | AUTH0_CLIENT_SECRET="${{ secrets.AUTH0_CLIENT_SECRET }}"
72 | AUTH0_ISSUER_BASE_URL="${{ secrets.AUTH0_ISSUER_BASE_URL }}"
73 | EOF
74 |
75 | - name: Build and push
76 | uses: docker/build-push-action@v6
77 | with:
78 | context: .
79 | platforms: linux/amd64
80 | push: true
81 | tags: ${{ steps.meta.outputs.tags }}
82 | labels: ${{ steps.meta.outputs.labels }}
83 | build-args: |
84 | BUILDKIT_INLINE_CACHE=1
85 | GIT_SHA=${{ github.sha }}
86 | cache-from: |
87 | type=gha,scope=dev
88 | type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-dev
89 | type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
90 | cache-to: |
91 | type=gha,mode=max,scope=dev
92 | type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-dev,mode=max
93 | secrets: |
94 | GIT_AUTH_TOKEN=${{ secrets.GITHUB_TOKEN }}
95 |
96 | - name: Clean up environment file
97 | if: always()
98 | run: rm -f .env
99 |
100 | - name: Build notification
101 | run: |
102 | echo "🚀 Docker image built and pushed successfully!"
103 | echo "📦 Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
104 | echo "🏷️ Tags: ${{ steps.meta.outputs.tags }}"
105 | echo "🏗️ Architecture: amd64"
106 |
107 | deploy:
108 | runs-on: ubuntu-latest
109 | needs: build
110 | if: success()
111 | permissions:
112 | contents: read
113 |
114 | steps:
115 | - name: Update container image on Magic Containers (CI/Testing)
116 | uses: BunnyWay/actions/container-update-image@main
117 | with:
118 | app_id: ${{ secrets.APP_ID_DEV }}
119 | api_key: ${{ secrets.BUNNYNET_API_KEY }}
120 | container: app
121 | image_tag: "sha-${{ github.sha }}"
122 |
123 | - name: Deploy notification
124 | run: |
125 | echo "🐰 CI Container updated on Magic Containers with SHA: ${{ github.sha }}"
126 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Output
4 | .output
5 | .vercel
6 | .netlify
7 | .wrangler
8 | /.svelte-kit
9 | /build
10 |
11 | # OS
12 | .DS_Store
13 | Thumbs.db
14 |
15 | # Env
16 | .env
17 | .env.*
18 | !.env.example
19 | !.env.test
20 |
21 | # Vite
22 | vite.config.js.timestamp-*
23 | vite.config.ts.timestamp-*
24 | oldsite/
25 | src/routes/api/seed/+server.ts
26 |
27 | # Utility Scripts (temporary/development only)
28 | check-db.js
29 | sync-data.js
30 | *.temp.js
31 | *.tmp.js
32 | scripts/dev.js
33 | *.md
34 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Package Managers
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
5 | bun.lock
6 | bun.lockb
7 |
8 | # Miscellaneous
9 | /static/
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7 | "overrides": [
8 | {
9 | "files": "*.svelte",
10 | "options": {
11 | "parser": "svelte"
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:alpine AS builder
2 | WORKDIR /app
3 |
4 | # Install pnpm globally and dumb-init for better process handling
5 | RUN npm install -g pnpm && \
6 | apk add --no-cache dumb-init
7 |
8 | # Copy package files
9 | COPY package.json pnpm-lock.yaml ./
10 |
11 | # Install dependencies with cache mount
12 | RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
13 | pnpm install --frozen-lockfile
14 |
15 | # Copy source code (exclude patterns handled by .dockerignore)
16 | COPY . .
17 |
18 | # Load environment variables and build with cache mount
19 | ARG GIT_SHA
20 | ENV PUBLIC_VITE_GIT_SHA=$GIT_SHA
21 | RUN --mount=type=cache,target=/app/.svelte-kit \
22 | export $(grep -v '^#' .env | xargs) && pnpm build
23 |
24 | # Prune to production dependencies with cache mount
25 | RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
26 | pnpm prune --prod
27 |
28 | FROM node:alpine
29 | WORKDIR /app
30 |
31 | # Install pnpm globally and dumb-init
32 | RUN npm install -g pnpm && \
33 | apk add --no-cache dumb-init
34 |
35 | # Copy built application and dependencies from builder
36 | COPY --from=builder /app/build build/
37 | COPY --from=builder /app/node_modules node_modules/
38 | COPY --from=builder /app/package.json .
39 |
40 | # Create non-root user for security
41 | RUN addgroup -g 1001 -S nodejs && \
42 | adduser -S sveltekit -u 1001
43 |
44 | # Change ownership to nodejs user
45 | RUN chown -R sveltekit:nodejs /app
46 |
47 | # Switch to non-root user
48 | USER sveltekit
49 |
50 | # Expose port
51 | EXPOSE 3000
52 |
53 | # Set production environment
54 | ENV NODE_ENV=production
55 | ENV PORT=3000
56 |
57 | # Start the application with dumb-init for proper signal handling and graceful shutdown
58 | CMD ["dumb-init", "node", "build"]
59 |
--------------------------------------------------------------------------------
/GITHUB_ACTIONS_README.md:
--------------------------------------------------------------------------------
1 | # GitHub Actions Docker Build Setup
2 |
3 | This repository includes automated Docker image building using GitHub Actions. The workflows build and push Docker images to GitHub Container Registry (ghcr.io).
4 |
5 | ## Workflows
6 |
7 | ### 1. `docker-build.yml` - Build and Test
8 | - **Triggers**: Push/PR to main/master branch
9 | - **Purpose**: Build Docker image for testing and development
10 | - **Registry**: Pushes to `ghcr.io/mediathekcommunity/website`
11 |
12 | ### 2. `deploy.yml` - Production Deployment
13 | - **Triggers**: Push to main/master branch or version tags
14 | - **Purpose**: Build and deploy production Docker images
15 | - **Environment**: Uses production environment protection
16 | - **Registry**: Pushes to `ghcr.io/mediathekcommunity/website`
17 |
18 | ## Required GitHub Secrets
19 |
20 | You need to set up the following secrets in your GitHub repository:
21 |
22 | ### For Development/Testing (`docker-build.yml`)
23 | Go to **Settings** → **Secrets and variables** → **Actions** → **Repository secrets**
24 |
25 | ```
26 | DATABASE_URL # Development database URL
27 | DATABASE_AUTH_TOKEN # Development database auth token
28 | PUBLIC_CLERK_PUBLISHABLE_KEY # Clerk publishable key
29 | CLERK_SECRET_KEY # Clerk secret key
30 | AUTH_SECRET # Auth.js secret
31 | AUTH0_CLIENT_ID # Auth0 client ID
32 | AUTH0_CLIENT_SECRET # Auth0 client secret
33 | AUTH0_ISSUER_BASE_URL # Auth0 issuer URL
34 | ```
35 |
36 | ### For Production (`deploy.yml`)
37 | Go to **Settings** → **Environments** → **Create environment: "production"**
38 |
39 | ```
40 | PROD_DATABASE_URL # Production database URL
41 | PROD_DATABASE_AUTH_TOKEN # Production database auth token
42 | PROD_PUBLIC_CLERK_PUBLISHABLE_KEY # Production Clerk publishable key
43 | PROD_CLERK_SECRET_KEY # Production Clerk secret key
44 | PROD_AUTH_SECRET # Production Auth.js secret
45 | PROD_AUTH0_CLIENT_ID # Production Auth0 client ID
46 | PROD_AUTH0_CLIENT_SECRET # Production Auth0 client secret
47 | PROD_AUTH0_ISSUER_BASE_URL # Production Auth0 issuer URL
48 | ```
49 |
50 | ## Setting Up Secrets
51 |
52 | ### 1. Repository Secrets (Development)
53 | 1. Go to your repository on GitHub
54 | 2. Click **Settings** → **Secrets and variables** → **Actions**
55 | 3. Click **New repository secret**
56 | 4. Add each secret from the development list above
57 |
58 | ### 2. Production Environment
59 | 1. Go to your repository on GitHub
60 | 2. Click **Settings** → **Environments**
61 | 3. Click **New environment** and name it "production"
62 | 4. Add protection rules (optional but recommended):
63 | - Required reviewers
64 | - Wait timer
65 | - Restrict to main branch
66 | 5. Add each secret from the production list above
67 |
68 | ## Docker Images
69 |
70 | The built images will be available at:
71 | ```
72 | ghcr.io/mediathekcommunity/website:latest # Latest main branch
73 | ghcr.io/mediathekcommunity/website:main # Main branch
74 | ghcr.io/mediathekcommunity/website:sha-abc123 # Specific commit
75 | ghcr.io/mediathekcommunity/website:production # Production release
76 | ```
77 |
78 | ## Running the Built Image
79 |
80 | To run a built image locally:
81 |
82 | ```bash
83 | # Pull the latest image
84 | docker pull ghcr.io/mediathekcommunity/website:latest
85 |
86 | # Run with your local environment
87 | docker run --env-file .env.prod -p 3000:3000 ghcr.io/mediathekcommunity/website:latest
88 | ```
89 |
90 | ## Security Features
91 |
92 | - ✅ **Secrets are never exposed** in build logs
93 | - ✅ **Environment files are cleaned up** after build
94 | - ✅ **Production environment protection** can be enabled
95 | - ✅ **Multi-architecture builds** (amd64, arm64)
96 | - ✅ **Build caching** for faster builds
97 | - ✅ **Automatic tagging** based on branch/commit
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://uithub.com/mediathekcommunity/website)
2 |
--------------------------------------------------------------------------------
/build-docker.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | setlocal
3 |
4 | REM Default to production
5 | set ENV_FILE=.env.prod
6 | set ENV_NAME=production
7 |
8 | REM Check for environment argument
9 | if "%1"=="dev" (
10 | set ENV_FILE=.env.dev
11 | set ENV_NAME=development
12 | ) else if "%1"=="development" (
13 | set ENV_FILE=.env.dev
14 | set ENV_NAME=development
15 | ) else if "%1"=="prod" (
16 | set ENV_FILE=.env.prod
17 | set ENV_NAME=production
18 | ) else if "%1"=="production" (
19 | set ENV_FILE=.env.prod
20 | set ENV_NAME=production
21 | ) else if not "%1"=="" (
22 | echo Invalid environment: %1
23 | echo Usage: %0 [dev^|prod]
24 | echo dev/development: Build for development
25 | echo prod/production: Build for production ^(default^)
26 | pause
27 | exit /b 1
28 | )
29 |
30 | echo Building Docker image for %ENV_NAME% environment...
31 |
32 | REM Check if environment file exists
33 | if not exist "%ENV_FILE%" (
34 | echo Error: %ENV_FILE% file not found. Please create it with your environment variables.
35 | pause
36 | exit /b 1
37 | )
38 |
39 | REM Create a backup of current .env if it exists
40 | if exist ".env" (
41 | copy .env .env.backup >nul
42 | )
43 |
44 | REM Copy the target environment file as .env for Docker build
45 | copy "%ENV_FILE%" .env >nul
46 |
47 | echo Using environment file: %ENV_FILE%
48 |
49 | REM Build the Docker image
50 | docker build -t media-app:%ENV_NAME% .
51 |
52 | set BUILD_RESULT=%ERRORLEVEL%
53 |
54 | REM Restore original .env file if backup exists
55 | if exist ".env.backup" (
56 | move .env.backup .env >nul
57 | ) else (
58 | REM If no backup, remove the temporary .env file
59 | del .env >nul 2>&1
60 | )
61 |
62 | if %BUILD_RESULT% EQU 0 (
63 | echo.
64 | echo Docker image built successfully for %ENV_NAME%!
65 | echo.
66 | echo To run the container:
67 | echo docker run --env-file %ENV_FILE% -p 3000:3000 media-app:%ENV_NAME%
68 | echo.
69 | echo Or use the run script:
70 | echo run-docker.bat %1
71 | ) else (
72 | echo Docker build failed!
73 | pause
74 | exit /b 1
75 | )
76 |
77 | pause
78 |
--------------------------------------------------------------------------------
/build-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Default to production
4 | ENV_FILE=".env.prod"
5 | ENV_NAME="production"
6 |
7 | # Check for environment argument
8 | if [ "$1" = "dev" ] || [ "$1" = "development" ]; then
9 | ENV_FILE=".env.dev"
10 | ENV_NAME="development"
11 | elif [ "$1" = "prod" ] || [ "$1" = "production" ]; then
12 | ENV_FILE=".env.prod"
13 | ENV_NAME="production"
14 | elif [ ! -z "$1" ]; then
15 | echo "Invalid environment: $1"
16 | echo "Usage: $0 [dev|prod]"
17 | echo " dev/development: Build for development"
18 | echo " prod/production: Build for production (default)"
19 | exit 1
20 | fi
21 |
22 | echo "Building Docker image for $ENV_NAME environment..."
23 |
24 | # Check if environment file exists
25 | if [ ! -f "$ENV_FILE" ]; then
26 | echo "Error: $ENV_FILE file not found. Please create it with your environment variables."
27 | exit 1
28 | fi
29 |
30 | # Create a backup of current .env if it exists
31 | if [ -f ".env" ]; then
32 | cp .env .env.backup
33 | fi
34 |
35 | # Copy the target environment file as .env for Docker build
36 | cp "$ENV_FILE" .env
37 |
38 | echo "Using environment file: $ENV_FILE"
39 |
40 | # Build the Docker image
41 | docker build -t media-app:$ENV_NAME .
42 |
43 | BUILD_RESULT=$?
44 |
45 | # Restore original .env file if backup exists
46 | if [ -f ".env.backup" ]; then
47 | mv .env.backup .env
48 | else
49 | # If no backup, remove the temporary .env file
50 | rm -f .env
51 | fi
52 |
53 | if [ $BUILD_RESULT -eq 0 ]; then
54 | echo "Docker image built successfully for $ENV_NAME!"
55 | echo ""
56 | echo "To run the container:"
57 | echo "docker run --env-file $ENV_FILE -p 3000:3000 media-app:$ENV_NAME"
58 | echo ""
59 | echo "Or use the run script:"
60 | echo "./run-docker.sh $1"
61 | else
62 | echo "Docker build failed!"
63 | exit 1
64 | fi
65 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | media-app:
5 | build: .
6 | ports:
7 | - "3000:3000"
8 | env_file:
9 | - .env
10 | environment:
11 | - NODE_ENV=production
12 | restart: unless-stopped
13 | # Uncomment the following lines if you need to mount volumes
14 | # volumes:
15 | # - ./data:/app/data
16 |
--------------------------------------------------------------------------------
/drizzle.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'drizzle-kit';
2 |
3 | /** @type { import("drizzle-kit").Config } */
4 | export default defineConfig({
5 | schema: './src/lib/server/schema.js',
6 | out: './drizzle',
7 | dialect: 'turso',
8 | dbCredentials: {
9 | url: process.env.NODE_ENV === 'development'
10 | ? (process.env.DATABASE_URL_DEV || process.env.DATABASE_URL)
11 | : process.env.DATABASE_URL,
12 | authToken: process.env.NODE_ENV === 'development'
13 | ? (process.env.DATABASE_AUTH_TOKEN_DEV || process.env.DATABASE_AUTH_TOKEN)
14 | : process.env.DATABASE_AUTH_TOKEN
15 | }
16 | });
--------------------------------------------------------------------------------
/drizzle/0000_square_sabretooth.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `CHANNELS` (
2 | `id` text PRIMARY KEY NOT NULL,
3 | `name` text NOT NULL,
4 | `title` text,
5 | `poster` text,
6 | `icon` text,
7 | `country` text
8 | );
9 | --> statement-breakpoint
10 | CREATE TABLE `EPISODES` (
11 | `id` text PRIMARY KEY NOT NULL,
12 | `series_id` text NOT NULL,
13 | `season_number` integer,
14 | `episode_number` integer,
15 | `title` text,
16 | `description` text,
17 | `original_video_url` text,
18 | `local_video_url` text,
19 | `release_date` text,
20 | `audio_language_format` text,
21 | `subtitles_info` text,
22 | `tmdbid` text,
23 | FOREIGN KEY (`series_id`) REFERENCES `MEDIA`(`id`) ON UPDATE no action ON DELETE cascade
24 | );
25 | --> statement-breakpoint
26 | CREATE TABLE `MEDIA` (
27 | `id` text PRIMARY KEY NOT NULL,
28 | `type` text NOT NULL,
29 | `title` text NOT NULL,
30 | `description` text,
31 | `poster_url` text,
32 | `backdrop_url` text,
33 | `genre` text,
34 | `release_date_year` text,
35 | `channel_id` text,
36 | `tmdbid` text,
37 | `cast` text,
38 | `crew` text,
39 | `online_until` text,
40 | FOREIGN KEY (`channel_id`) REFERENCES `CHANNELS`(`id`) ON UPDATE no action ON DELETE no action
41 | );
42 | --> statement-breakpoint
43 | CREATE TABLE `MOVIES_FILES` (
44 | `id` text PRIMARY KEY NOT NULL,
45 | `movie_id` text NOT NULL,
46 | `video_url` text NOT NULL,
47 | `local_video_url` text,
48 | `quality` text,
49 | `format` text,
50 | `audio_language_format` text,
51 | `subtitles_info` text,
52 | FOREIGN KEY (`movie_id`) REFERENCES `MEDIA`(`id`) ON UPDATE no action ON DELETE cascade
53 | );
54 |
--------------------------------------------------------------------------------
/drizzle/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "7",
3 | "dialect": "sqlite",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "6",
8 | "when": 1753130137362,
9 | "tag": "0000_square_sabretooth",
10 | "breakpoints": true
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/drizzle/relations.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm/relations";
2 | import { media, episodes, channels, moviesFiles } from "./schema";
3 |
4 | export const episodesRelations = relations(episodes, ({one}) => ({
5 | media: one(media, {
6 | fields: [episodes.seriesId],
7 | references: [media.id]
8 | }),
9 | }));
10 |
11 | export const mediaRelations = relations(media, ({one, many}) => ({
12 | episodes: many(episodes),
13 | channel: one(channels, {
14 | fields: [media.channelId],
15 | references: [channels.id]
16 | }),
17 | moviesFiles: many(moviesFiles),
18 | }));
19 |
20 | export const channelsRelations = relations(channels, ({many}) => ({
21 | media: many(media),
22 | }));
23 |
24 | export const moviesFilesRelations = relations(moviesFiles, ({one}) => ({
25 | media: one(media, {
26 | fields: [moviesFiles.movieId],
27 | references: [media.id]
28 | }),
29 | }));
--------------------------------------------------------------------------------
/drizzle/schema.ts:
--------------------------------------------------------------------------------
1 | import { sqliteTable, AnySQLiteColumn, text, foreignKey, integer } from "drizzle-orm/sqlite-core"
2 | import { sql } from "drizzle-orm"
3 |
4 | export const channels = sqliteTable("CHANNELS", {
5 | id: text().primaryKey().notNull(),
6 | name: text().notNull(),
7 | title: text(),
8 | poster: text(),
9 | icon: text(),
10 | country: text(),
11 | });
12 |
13 | export const episodes = sqliteTable("EPISODES", {
14 | id: text().primaryKey().notNull(),
15 | seriesId: text("series_id").notNull().references(() => media.id, { onDelete: "cascade" } ),
16 | seasonNumber: integer("season_number"),
17 | episodeNumber: integer("episode_number"),
18 | title: text(),
19 | description: text(),
20 | originalVideoUrl: text("original_video_url"),
21 | localVideoUrl: text("local_video_url"),
22 | releaseDate: text("release_date"),
23 | audioLanguageFormat: text("audio_language_format"),
24 | subtitlesInfo: text("subtitles_info"),
25 | tmdbid: text(),
26 | });
27 |
28 | export const media = sqliteTable("MEDIA", {
29 | id: text().primaryKey().notNull(),
30 | type: text().notNull(),
31 | title: text().notNull(),
32 | description: text(),
33 | posterUrl: text("poster_url"),
34 | backdropUrl: text("backdrop_url"),
35 | genre: text(),
36 | releaseDateYear: text("release_date_year"),
37 | channelId: text("channel_id").references(() => channels.id),
38 | tmdbid: text(),
39 | cast: text(),
40 | crew: text(),
41 | onlineUntil: text("online_until"),
42 | });
43 |
44 | export const moviesFiles = sqliteTable("MOVIES_FILES", {
45 | id: text().primaryKey().notNull(),
46 | movieId: text("movie_id").notNull().references(() => media.id, { onDelete: "cascade" } ),
47 | videoUrl: text("video_url").notNull(),
48 | localVideoUrl: text("local_video_url"),
49 | quality: text(),
50 | format: text(),
51 | audioLanguageFormat: text("audio_language_format"),
52 | subtitlesInfo: text("subtitles_info"),
53 | });
54 |
55 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import prettier from 'eslint-config-prettier';
2 | import { includeIgnoreFile } from '@eslint/compat';
3 | import js from '@eslint/js';
4 | import svelte from 'eslint-plugin-svelte';
5 | import globals from 'globals';
6 | import { fileURLToPath } from 'node:url';
7 | import svelteConfig from './svelte.config.js';
8 |
9 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
10 |
11 | /** @type {import('eslint').Linter.Config[]} */
12 | export default [
13 | includeIgnoreFile(gitignorePath),
14 | js.configs.recommended,
15 | ...svelte.configs.recommended,
16 | prettier,
17 | ...svelte.configs.prettier,
18 | {
19 | languageOptions: {
20 | globals: { ...globals.browser, ...globals.node }
21 | }
22 | },
23 | {
24 | files: ['**/*.svelte', '**/*.svelte.js'],
25 | languageOptions: { parserOptions: { svelteConfig } }
26 | }
27 | ];
28 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler"
13 | }
14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
16 | //
17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18 | // from the referenced tsconfig.json - TypeScript does not merge them in
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "media-app",
3 | "private": true,
4 | "version": "0.0.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "node scripts/dev.js",
8 | "build": "vite build",
9 | "start": "node build",
10 | "preview": "vite preview",
11 | "prepare": "svelte-kit sync || echo ''",
12 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
13 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
14 | "format": "prettier --write .",
15 | "lint": "prettier --check . && eslint ."
16 | },
17 | "devDependencies": {
18 | "@eslint/compat": "^1.3.1",
19 | "@eslint/js": "^9.32.0",
20 | "@sveltejs/adapter-node": "^5.2.13",
21 | "@sveltejs/kit": "^2.27.0",
22 | "@sveltejs/vite-plugin-svelte": "^6.1.0",
23 | "@tailwindcss/vite": "^4.1.11",
24 | "@types/node": "^24.1.0",
25 | "dotenv": "^17.2.1",
26 | "eslint": "^9.32.0",
27 | "eslint-config-prettier": "^10.1.8",
28 | "eslint-plugin-svelte": "^3.11.0",
29 | "globals": "^16.3.0",
30 | "prettier": "^3.6.2",
31 | "prettier-plugin-svelte": "^3.4.0",
32 | "prettier-plugin-tailwindcss": "^0.6.14",
33 | "svelte": "^5.37.2",
34 | "svelte-check": "^4.3.0",
35 | "tailwindcss": "^4.1.11",
36 | "typescript": "^5.9.2",
37 | "vite": "^6.3.5"
38 | },
39 | "pnpm": {
40 | "onlyBuiltDependencies": [
41 | "@tailwindcss/oxide",
42 | "esbuild",
43 | "sharp",
44 | "workerd"
45 | ]
46 | },
47 | "dependencies": {
48 | "@auth/sveltekit": "^1.10.0",
49 | "@iconify/svelte": "^5.0.1",
50 | "@libsql/client": "^0.15.10",
51 | "@unpic/svelte": "^1.0.0",
52 | "daisyui": "^5.0.50",
53 | "drizzle-kit": "^0.31.4",
54 | "drizzle-orm": "^0.44.4",
55 | "embla-carousel-autoplay": "^8.6.0",
56 | "embla-carousel-fade": "^8.6.0",
57 | "embla-carousel-svelte": "^8.6.0",
58 | "uuid": "^11.1.0",
59 | "video.js": "^8.23.3",
60 | "wrangler": "^4.27.0",
61 | "zod": "^3.25.76"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/run-docker.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | setlocal enabledelayedexpansion
3 |
4 | REM Default to production
5 | set ENV_FILE=.env.prod
6 | set ENV_NAME=production
7 | set IMAGE_TAG=media-app:production
8 |
9 | REM Check for environment argument
10 | if "%1"=="dev" (
11 | set ENV_FILE=.env.dev
12 | set ENV_NAME=development
13 | set IMAGE_TAG=media-app:development
14 | ) else if "%1"=="development" (
15 | set ENV_FILE=.env.dev
16 | set ENV_NAME=development
17 | set IMAGE_TAG=media-app:development
18 | ) else if "%1"=="prod" (
19 | set ENV_FILE=.env.prod
20 | set ENV_NAME=production
21 | set IMAGE_TAG=media-app:production
22 | ) else if "%1"=="production" (
23 | set ENV_FILE=.env.prod
24 | set ENV_NAME=production
25 | set IMAGE_TAG=media-app:production
26 | ) else if not "%1"=="" (
27 | echo Invalid environment: %1
28 | echo Usage: %0 [dev^|prod]
29 | echo dev/development: Run development image
30 | echo prod/production: Run production image ^(default^)
31 | pause
32 | exit /b 1
33 | )
34 |
35 | echo Starting %ENV_NAME% container...
36 |
37 | REM Check if environment file exists
38 | if not exist "%ENV_FILE%" (
39 | echo Error: %ENV_FILE% file not found.
40 | pause
41 | exit /b 1
42 | )
43 |
44 | echo Loading environment variables from %ENV_FILE%...
45 |
46 | REM Read environment file and set variables
47 | for /f "usebackq tokens=1,2 delims==" %%i in ("%ENV_FILE%") do (
48 | set "line=%%i"
49 | if not "!line:~0,1!"=="#" (
50 | set "%%i=%%j"
51 | set "%%i=!%%i:"=!"
52 | )
53 | )
54 |
55 | echo Running Docker container...
56 |
57 | docker run ^
58 | -e DATABASE_URL="%DATABASE_URL%" ^
59 | -e DATABASE_AUTH_TOKEN="%DATABASE_AUTH_TOKEN%" ^
60 | -e PUBLIC_CLERK_PUBLISHABLE_KEY="%PUBLIC_CLERK_PUBLISHABLE_KEY%" ^
61 | -e CLERK_SECRET_KEY="%CLERK_SECRET_KEY%" ^
62 | -e AUTH_SECRET="%AUTH_SECRET%" ^
63 | -e AUTH0_CLIENT_ID="%AUTH0_CLIENT_ID%" ^
64 | -e AUTH0_CLIENT_SECRET="%AUTH0_CLIENT_SECRET%" ^
65 | -e AUTH0_ISSUER_BASE_URL="%AUTH0_ISSUER_BASE_URL%" ^
66 | -e NODE_ENV="%NODE_ENV%" ^
67 | -p 3000:3000 ^
68 | "%IMAGE_TAG%"
69 |
70 | echo.
71 | echo Container started on http://localhost:3000
72 |
73 | pause
74 |
--------------------------------------------------------------------------------
/run-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Default to production
4 | ENV_FILE=".env.prod"
5 | ENV_NAME="production"
6 | IMAGE_TAG="media-app:production"
7 |
8 | # Check for environment argument
9 | if [ "$1" = "dev" ] || [ "$1" = "development" ]; then
10 | ENV_FILE=".env.dev"
11 | ENV_NAME="development"
12 | IMAGE_TAG="media-app:development"
13 | elif [ "$1" = "prod" ] || [ "$1" = "production" ]; then
14 | ENV_FILE=".env.prod"
15 | ENV_NAME="production"
16 | IMAGE_TAG="media-app:production"
17 | elif [ ! -z "$1" ]; then
18 | echo "Invalid environment: $1"
19 | echo "Usage: $0 [dev|prod]"
20 | echo " dev/development: Run development image"
21 | echo " prod/production: Run production image (default)"
22 | exit 1
23 | fi
24 |
25 | echo "Starting $ENV_NAME container..."
26 |
27 | # Check if environment file exists
28 | if [ ! -f "$ENV_FILE" ]; then
29 | echo "Error: $ENV_FILE file not found."
30 | exit 1
31 | fi
32 |
33 | # Load environment variables from the specified file
34 | if [ -f "$ENV_FILE" ]; then
35 | export $(grep -v '^#' "$ENV_FILE" | xargs)
36 | fi
37 |
38 | # Run the Docker container with environment variables
39 | docker run \
40 | -e DATABASE_URL="$DATABASE_URL" \
41 | -e DATABASE_AUTH_TOKEN="$DATABASE_AUTH_TOKEN" \
42 | -e PUBLIC_CLERK_PUBLISHABLE_KEY="$PUBLIC_CLERK_PUBLISHABLE_KEY" \
43 | -e CLERK_SECRET_KEY="$CLERK_SECRET_KEY" \
44 | -e AUTH_SECRET="$AUTH_SECRET" \
45 | -e AUTH0_CLIENT_ID="$AUTH0_CLIENT_ID" \
46 | -e AUTH0_CLIENT_SECRET="$AUTH0_CLIENT_SECRET" \
47 | -e AUTH0_ISSUER_BASE_URL="$AUTH0_ISSUER_BASE_URL" \
48 | -e NODE_ENV="$NODE_ENV" \
49 | -p 3000:3000 \
50 | "$IMAGE_TAG"
51 |
52 | echo "Container started on http://localhost:3000"
53 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @plugin "daisyui" {
3 | themes: dark --default, light;
4 | }
5 |
6 | /* Global styles für bessere Übersicht */
7 | :global(body) {
8 | margin: 0;
9 | padding: 0;
10 | box-sizing: border-box;
11 | background-color: #141414;
12 | color: #fff;
13 | font-family: Arial, sans-serif;
14 | }
15 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://svelte.dev/docs/kit/types#app.d.ts
2 | // for information about these interfaces
3 | import type { Security } from './hooks.server';
4 |
5 | declare global {
6 | namespace App {
7 | // interface Error {}
8 | // interface Locals {}
9 | // interface PageData {}
10 | // interface PageState {}
11 | interface Platform {
12 | env: {
13 | DATABASE_URL: string;
14 | DATABASE_AUTH_TOKEN: string;
15 | DATABASE_URL_DEV?: string;
16 | DATABASE_AUTH_TOKEN_DEV?: string;
17 | NODE_ENV?: string;
18 | AUTH_SECRET: string;
19 | // Add other environment variables as needed
20 | };
21 | }
22 | }
23 | }
24 |
25 | export {};
26 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | import { SvelteKitAuth } from '@auth/sveltekit';
2 | import Auth0 from '@auth/sveltekit/providers/auth0';
3 |
4 | // Function to load environment variables from .env file if needed
5 | function loadEnvVar(key: string): string {
6 | // Try process.env first
7 | let value = process.env[key];
8 |
9 | // If not found and we're in a build environment, try to read from .env file
10 | if (!value && typeof window === 'undefined') {
11 | try {
12 | // Only available during build time if .env is copied
13 | const fs = require('fs');
14 | const path = require('path');
15 | const envPath = path.join(process.cwd(), '.env');
16 |
17 | if (fs.existsSync(envPath)) {
18 | const envFile = fs.readFileSync(envPath, 'utf8');
19 | const match = envFile.match(new RegExp(`^${key}=(.*)$`, 'm'));
20 | if (match) {
21 | value = match[1].replace(/^"(.*)"$/, '$1'); // Remove quotes if present
22 | }
23 | }
24 | } catch (error) {
25 | // Ignore errors, fallback to empty string
26 | }
27 | }
28 |
29 | return value || '';
30 | }
31 |
32 | export const { handle, signIn, signOut } = SvelteKitAuth({
33 | providers: [
34 | Auth0({
35 | clientId: loadEnvVar('AUTH0_CLIENT_ID'),
36 | clientSecret: loadEnvVar('AUTH0_CLIENT_SECRET'),
37 | issuer: loadEnvVar('AUTH0_ISSUER_BASE_URL')
38 | })
39 | ],
40 | trustHost: true // Set to true if you trust the host
41 | });
42 |
--------------------------------------------------------------------------------
/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | import { type Handle } from '@sveltejs/kit';
2 | import { handle as authHandle } from "./auth";
3 |
4 | export const handle: Handle = async ({ event, resolve }) => {
5 | // First, let auth handle authentication
6 | const response = await authHandle({ event, resolve });
7 |
8 | // Then add cache headers for better performance
9 | if (event.request.method === 'GET') {
10 | const url = new URL(event.request.url);
11 |
12 | // Static assets - long cache (1 year)
13 | if (url.pathname.startsWith('/_app/') ||
14 | url.pathname.startsWith('/favicon') ||
15 | url.pathname.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|webp|avif)$/)) {
16 | response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
17 | response.headers.set('Vary', 'Accept-Encoding');
18 | }
19 | // API routes - short cache with revalidation
20 | else if (url.pathname.startsWith('/api/')) {
21 | response.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
22 | response.headers.set('Vary', 'Accept-Encoding');
23 | }
24 | // Admin pages - no cache (sensitive)
25 | else if (url.pathname.startsWith('/admin/')) {
26 | response.headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate');
27 | response.headers.set('Pragma', 'no-cache');
28 | response.headers.set('Expires', '0');
29 | }
30 | // Media/video content - longer cache
31 | else if (url.pathname.startsWith('/player/') || url.pathname.startsWith('/details/')) {
32 | response.headers.set('Cache-Control', 'public, max-age=1800, stale-while-revalidate=7200');
33 | response.headers.set('Vary', 'Accept-Encoding');
34 | }
35 | // Regular pages - moderate cache with revalidation
36 | else {
37 | response.headers.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=3600');
38 | response.headers.set('Vary', 'Accept-Encoding');
39 | }
40 | }
41 |
42 | // Add security headers
43 | response.headers.set('X-Content-Type-Options', 'nosniff');
44 | response.headers.set('X-Frame-Options', 'DENY');
45 | response.headers.set('X-XSS-Protection', '1; mode=block');
46 |
47 | return response;
48 | };
49 |
--------------------------------------------------------------------------------
/src/lib/components/CountrySlider.svelte:
--------------------------------------------------------------------------------
1 |
34 |
35 | {#each langlist as country}
36 | {#if langdata[country]?.length > 0}
37 |
38 |
39 | {getCountryName(country)}
40 |
41 |
42 |
43 |
44 | {#each langdata[country] as item (item.id)}
45 |
46 |
47 |
48 | {/each}
49 |
50 |
51 |
52 | {/if}
53 | {/each}
54 |
55 |
103 |
--------------------------------------------------------------------------------
/src/lib/components/Footer.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
98 |
99 |
--------------------------------------------------------------------------------
/src/lib/components/MediaCard.svelte:
--------------------------------------------------------------------------------
1 |
94 |
95 |
96 |
101 |
102 | {#each heroItems as slide (slide.id)}
103 |
104 |
105 |
116 |
117 |
120 |
121 | {#if slide.channel}
122 |
123 | {#if slide.channel.icon}
124 |
125 | {/if}
126 |
127 | {/if}
128 |
129 | {#if slide.quality}
130 |
133 |
134 |
135 | {/if}
136 |
139 |
140 |
141 |
142 |
143 |
146 | {slide.title}
147 |
148 | {#if slide.orgtitle && slide.orgtitle !== slide.title}
149 |
150 | Original Title: {slide.orgtitle}
151 |
152 | {/if}
153 |
154 |
155 | {slide.description}
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | {/each}
165 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/src/lib/components/MediaCarouselCard.svelte:
--------------------------------------------------------------------------------
1 |
62 |
63 | {#key mediaItem.id}
64 |
65 | (isHovered = true)}
70 | onmouseleave={() => (isHovered = false)}
71 | >
72 |
73 |

74 |
75 |
76 | {#if mediaItem.channel}
77 |
78 | {#if mediaItem.channel.icon}
79 |
80 | {/if}
81 |
82 | {/if}
83 |
84 |
85 |
86 |
87 |
88 |
89 | {#if isHovered}
90 |
91 |
{mediaItem.title}
92 | {#if mediaItem.original_title && mediaItem.original_title !== mediaItem.title}
93 |
{mediaItem.original_title}
94 | {/if}
95 |
96 |
97 | {/if}
98 |
99 | {#if mediaItem.online_until}
100 |
101 | {#if daysLeft <= 1}
102 |
103 | {daysLeft} day left
104 |
105 | {:else if daysLeft <= 3}
106 |
107 | {daysLeft} days left
108 |
109 | {/if}
110 |
111 | {/if}
112 |
113 |
114 | {/key}
115 |
116 |
--------------------------------------------------------------------------------
/src/lib/components/MediaItemCard.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |

7 |
8 |
9 |
10 | {#if mediaItem.logo_url}
11 |
12 |
13 |
14 | {/if}
15 | {#if mediaItem.uhd}
16 |
17 | UHD
18 |
19 | {/if}
20 | {#if mediaItem.type === 'movie'}
21 |
22 |
25 |
26 | {/if}
27 |
28 |
29 | {mediaItem.title}
30 |
31 | {#if mediaItem.original_title && mediaItem.original_title !== ''}
32 |
33 | Original Title: {mediaItem.original_title}
34 |
35 | {/if}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/lib/components/Navbar.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
11 |
12 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | // place files you want to import through the `$lib` alias in this folder.
2 |
--------------------------------------------------------------------------------
/src/lib/schemas/channel.js:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const channelSchema = z.object({
4 | id: z.string(),
5 | name: z.string().nonempty('Channel name cannot be empty'),
6 | title: z.string().optional(),
7 | poster: z.string().optional(),
8 | icon: z.string().optional(),
9 | country: z
10 | .string()
11 | .length(2, 'Country code must be 2 characters')
12 | .refine(
13 | /** @param {string} val */
14 | (val) =>
15 | ['AT', 'BE', 'FR', 'DE', 'IE', 'IT', 'LU', 'NL', 'PT', 'ES', 'CH', 'GB', 'SE'].includes(
16 | val
17 | ),
18 | { message: 'Invalid Western European country code' }
19 | )
20 | });
21 |
--------------------------------------------------------------------------------
/src/lib/server/db.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@libsql/client';
2 | import { drizzle } from 'drizzle-orm/libsql';
3 | import * as schema from './schema';
4 |
5 | // Get environment variables with fallbacks
6 | const DATABASE_URL = process.env.DATABASE_URL;
7 | const DATABASE_AUTH_TOKEN = process.env.DATABASE_AUTH_TOKEN;
8 | const NODE_ENV = process.env.NODE_ENV || 'development';
9 |
10 | // Function to create database client with platform support
11 | export function createDatabase(platform?: App.Platform) {
12 | // Use platform.env if available (for Cloudflare Workers), otherwise use process.env
13 | const databaseUrl = platform?.env?.DATABASE_URL || DATABASE_URL;
14 | const authToken = platform?.env?.DATABASE_AUTH_TOKEN || DATABASE_AUTH_TOKEN;
15 |
16 | if (!databaseUrl || !authToken) {
17 | throw new Error(
18 | `Database connection failed. Missing: ${!databaseUrl ? 'DATABASE_URL' : ''} ${!authToken ? 'DATABASE_AUTH_TOKEN' : ''}`.trim()
19 | );
20 | }
21 |
22 | console.log(`Connecting to database in ${NODE_ENV} environment`);
23 |
24 | const client = createClient({
25 | url: databaseUrl,
26 | authToken: authToken
27 | });
28 |
29 | return drizzle(client, { schema });
30 | }
31 |
32 | // Default database instance
33 | const db = createDatabase();
34 |
35 | export default db;
36 |
--------------------------------------------------------------------------------
/src/lib/server/schema.js:
--------------------------------------------------------------------------------
1 | import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
2 | import { relations } from 'drizzle-orm';
3 |
4 | export const channels = sqliteTable('CHANNELS', {
5 | id: text('id').primaryKey(),
6 | name: text('name').notNull(),
7 | title: text('title'),
8 | poster: text('poster'),
9 | icon: text('icon'),
10 | country: text('country'),
11 | });
12 |
13 | export const media = sqliteTable('MEDIA', {
14 | id: text('id').primaryKey(),
15 | type: text('type').notNull(), // "movie" or "series"
16 | title: text('title').notNull(),
17 | description: text('description'),
18 | poster_url: text('poster_url'),
19 | backdrop_url: text('backdrop_url'),
20 | genre: text('genre'),
21 | release_date_year: text('release_date_year'),
22 | channelId: text('channel_id').references(() => channels.id),
23 | tmdbid: text('tmdbid'),
24 | cast: text('cast'),
25 | crew: text('crew'),
26 | onlineUntil: text('online_until'),
27 | });
28 |
29 | export const moviesFiles = sqliteTable('MOVIES_FILES', {
30 | id: text('id').primaryKey(),
31 | movieId: text('movie_id').notNull().references(() => media.id, { onDelete: 'cascade' }),
32 | videoUrl: text('video_url').notNull(),
33 | localVideoUrl: text('local_video_url'),
34 | quality: text('quality'),
35 | format: text('format'),
36 | audioLanguageFormat: text('audio_language_format'),
37 | subtitlesInfo: text('subtitles_info'),
38 | });
39 |
40 | export const episodes = sqliteTable('EPISODES', {
41 | id: text('id').primaryKey(),
42 | seriesId: text('series_id').notNull().references(() => media.id, { onDelete: 'cascade' }),
43 | seasonNumber: integer('season_number'),
44 | episodeNumber: integer('episode_number'),
45 | title: text('title'),
46 | description: text('description'),
47 | originalVideoUrl: text('original_video_url'),
48 | localVideoUrl: text('local_video_url'),
49 | releaseDate: text('release_date'),
50 | audioLanguageFormat: text('audio_language_format'),
51 | subtitlesInfo: text('subtitles_info'),
52 | tmdbid: text('tmdbid'),
53 | });
54 |
55 | export const channelsRelations = relations(channels, ({ many }) => ({
56 | media: many(media),
57 | }));
58 |
59 | export const mediaRelations = relations(media, ({ one, many }) => ({
60 | moviesFiles: many(moviesFiles),
61 | episodes: many(episodes),
62 | channel: one(channels, {
63 | fields: [media.channelId],
64 | references: [channels.id],
65 | }),
66 | }));
67 |
68 | export const moviesFilesRelations = relations(moviesFiles, ({ one }) => ({
69 | movie: one(media, {
70 | fields: [moviesFiles.movieId],
71 | references: [media.id],
72 | }),
73 | }));
74 |
75 | export const episodesRelations = relations(episodes, ({ one }) => ({
76 | series: one(media, {
77 | fields: [episodes.seriesId],
78 | references: [media.id],
79 | }),
80 | }));
--------------------------------------------------------------------------------
/src/lib/store.ts:
--------------------------------------------------------------------------------
1 | import { writable, type Writable } from 'svelte/store';
2 |
3 | // Modal and video states
4 | interface ModalProps {
5 | isOpen?: boolean;
6 | title?: string;
7 | content?: any;
8 | [key: string]: any;
9 | }
10 |
11 | interface VideoData {
12 | src?: string;
13 | thumb?: string;
14 | poster?: string;
15 | type?: string;
16 | title?: string;
17 | tracks?: Array<{
18 | kind: string;
19 | src: string;
20 | label: string;
21 | srclang?: string;
22 | }>;
23 | // New schema fields
24 | id?: string;
25 | quality?: string;
26 | format?: string;
27 | audioLanguageFormat?: string;
28 | subtitlesInfo?: string;
29 | }
30 |
31 | interface PlaylistItem {
32 | // Base video properties
33 | src?: string;
34 | thumb?: string;
35 | poster?: string;
36 | type?: string;
37 | title?: string;
38 | tracks?: Array<{
39 | kind: string;
40 | src: string;
41 | label: string;
42 | srclang?: string;
43 | }>;
44 | // Schema fields
45 | id: string | number;
46 | quality?: string;
47 | format?: string;
48 | audioLanguageFormat?: string;
49 | subtitlesInfo?: string;
50 | // Episode-specific fields for series
51 | seasonNumber?: number;
52 | episodeNumber?: number;
53 | episodeTitle?: string;
54 | description?: string;
55 | originalVideoUrl?: string;
56 | localVideoUrl?: string;
57 | releaseDate?: string;
58 | }
59 |
60 | // Subtitle and track interface
61 | interface SubtitleData {
62 | language?: string;
63 | src?: string;
64 | label?: string;
65 | default?: boolean;
66 | }
67 |
68 | // Type-safe store exports
69 | export const modalProps: Writable = writable({});
70 | export const modalvideo: Writable = writable(null);
71 | export const playlist: Writable = writable([]);
72 | export const playlistindex: Writable = writable(0);
73 | export const subtitle: Writable = writable({});
74 | export const seriestype: Writable<'playlist' | 'single' | 'default'> = writable('default');
75 | export const subs: Writable = writable([]);
76 | export const detailsid: Writable = writable(null);
77 | export const playlistov: Writable = writable(false);
78 |
79 | // Navigation state
80 | export const nav1 = writable('0');
81 |
82 | // Filter and visibility states
83 | export const alllang = writable(false);
84 | export const visible = writable(false);
85 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | export function groupByBroadcastCompany(mediaItems: any[]) {
2 | const grouped: { [key: string]: any[] } = {};
3 | mediaItems.forEach(item => {
4 | const company = item.broadcast_company || 'Unknown';
5 | if (!grouped[company]) {
6 | grouped[company] = [];
7 | }
8 | grouped[company].push(item);
9 | });
10 | return grouped;
11 | }
--------------------------------------------------------------------------------
/src/lib/utils/countryNames.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Maps a country code to its full English name using Intl.DisplayNames.
3 | * @param code - The 2-letter country code.
4 | * @returns The full English name of the country.
5 | */
6 | const displayNames = new Intl.DisplayNames(['en'], { type: 'region' });
7 | export function getCountryName(code) {
8 | return displayNames.of(code) || 'Unknown';
9 | }
--------------------------------------------------------------------------------
/src/lib/utils/countryNames.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Maps a country code to its full English name using Intl.DisplayNames.
3 | * @param code - The 2-letter country code.
4 | * @returns The full English name of the country.
5 | */
6 | const displayNames = new Intl.DisplayNames(['en'], { type: 'region' });
7 | export function getCountryName(code: string): string {
8 | return displayNames.of(code) || 'Unknown';
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/liveclock.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2023 The Nuevodevel Team. All rights reserved.
3 | * LiveClock plugin for video.js
4 | * Version 1.0.0
5 | */
6 | /* eslint-disable */import videojs from"video.js";const defaults={position:"controlbar",direction:"left",offsetV:15,offsetH:15,background:"rgba(0,0,0,.5)",fontSize:16,hours:12,locale:"en-US",timeZone:"",seconds:!1,seekable:!0},onPlayerReady=function(A,e){var o,i=null,t=!1,n=!1,l="";A.timeZone=function(A){e.timeZone=A;if(i)s()};A.clockLocale=function(A){e.locale=A};A.clockHours=function(A){if(24===A||12===A){e.hours=A;if(i)s()}};A.clockSeconds=function(A){e.seconds=A;if(i)s()};function s(){var A=new Date(),o=!0,t="";if(24===e.hours)o=!1;if(""!==e.timeZone)if(e.seconds)t=A.toLocaleString(e.locale,{timeZone:e.timeZone,hour:"numeric",minute:"numeric",second:"numeric",hour12:o});else t=A.toLocaleString(e.locale,{timeZone:e.timeZone,hour:"numeric",minute:"numeric",hour12:o});else if(e.seconds)t=A.toLocaleString(e.locale,{hour:"numeric",minute:"numeric",second:"numeric",hour12:o});else t=A.toLocaleString(e.locale,{hour:"numeric",minute:"numeric",hour12:o});i.innerHTML=t}function c(){if(i){s();clearTimeout(o);o=setTimeout(c,1e3)}}A.on("playing",function(){if(i)i.classList.remove("vjs-hidden")});A.on("loadeddata",function(){if(l!==A.currentSrc()){l=A.currentSrc();a()}});A.one("play",function(){a()});function r(){if(videojs.dom.hasClass(A.el(),"vjs-liveui"))t=!0;if(videojs.dom.hasClass(A.el(),"vjs-live"))n=!0;if(n)if(!0!==t||!0===e.seekable)return!0;return!1}function a(){if(videojs.dom.hasClass(A.el(),"vjs-liveui"))t=!0;if(videojs.dom.hasClass(A.el(),"vjs-live"))n=!0;if(!r())if(i)i.classList.add("vjs-hidden");if(o)clearTimeout(o);if(r()){if(!i){var l="@font-face{font-family:clock;src:url('data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAQ4AA0AAAAABcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAEHAAAABsAAAAclWCRe0dERUYAAAP8AAAAHgAAAB4AKQALT1MvMgAAAaQAAABGAAAAVmNwZjVjbWFwAAACAAAAAEMAAAFSADXv02dhc3AAAAP0AAAACAAAAAj//wADZ2x5ZgAAAlAAAACrAAAAwEBVZNRoZWFkAAABMAAAADIAAAA2Jtu0xGhoZWEAAAFkAAAAHQAAACQOugcGaG10eAAAAewAAAATAAAAFBKqAFVsb2NhAAACRAAAAAwAAAAMAAAAYG1heHAAAAGEAAAAHgAAACAASgA0bmFtZQAAAvwAAADcAAABa5oq/91wb3N0AAAD2AAAABwAAAAyaNltc3jaY2BkYGAA4mUtberx/DZfGbg5GEDg/j1TMyhtwxD6n4F9NVsokMvBwAQSBQAmRApNAAB42mNgZGBgC/3PwLCDgwEE2FczMDKgAlYASr8C1AAAAHjaY2BkYGBgZTBkYGYAASYgZmQAiTmA+QwAB84AfwAAeNpjYORgYJzAwMrAwGrMOpOBgVEOQjNfZ0hjEmJgYGJgZWaAAUYBBgQISHNNYXBgUHjJwJb2L41hB1soI1gNiAAAbVAJZwAAeNrjYIAAplUQmoOBIRQACI8BEgB42mNgYGBmgGAZBkYGEPAB8hjBfBYGAyDNAYRMYBmFlwz//yOz/j8WZ4HqAgNGNgY4lxGkh4kBFTBCrBrOAAD9CwjzAAAAAAAAAAAAAAAAYHjaY2BmCP3PwL6aLZSBn0GewYCBgUGAiUFFgYWBSUCIgUVBBUgzsQFpNSDfDCgupiooIs4qZmZupqauKmiipsTGvnrivz//Vv5rAuI/EycysjCGM9YBMYtU+ZV/z/+d+Pf8Snn5FUZJRgtGySt/lfQZQ+VdBATEFP7dNHIzYnwDVBvGyDIRZMYqhBlMm9A0Alne/xhMJVYJiMsrOAlMZjyjbGTEwAAARuM+ogB42m2OPWrDQBCFP9mSQ35I4cL14jIgIa3BhatUPoAL90YswkRoYW0fI20gVY6RA+QAvlKe5AUX9sLsfDO8mTfAC18k9C8hU3XhEQ/MI48xtJFTab4jZzzzG3mi6ixlkj6q8zRM9TzilWnkMe+8RU6l+YycMeMn8kT9P2r5ef0fULe+VtrgaDipvyOodM2p3QnW0nUchxykcLrVUlAqrxTXTZd6SU6lsFJYFlrgu+Pah8YZW5RmZQY/5WVe5ba0Utwes5VN4MB+MDda2BuydeGw952pivLO1D/cmDF6eNpjYGLAD1iBmJGBiYGZkYk1OSc/ORsAB3gCIgAAAAH//wACAAEAAAAMAAAAFgAAAAIAAQADAAQAAQAEAAAAAgAAAAB42mNgYGBkAIKrS9Q5QPT9e6ZmUNoGAD79BgAA') format('woff');font-weight:400;font-style:normal;font-display:swap}.vjs-clock:before{font-family:clock;content:'\\e900';padding-right:8px}",a=document.createElement("style");a.textContent=l;document.head.appendChild(a);(i=document.createElement("div")).className="vjs-live-control vjs-clock vjs-hidden";i.style.textAlign="left";i.style.padding="0 8px";if("controlbar"===e.position){var f=A.controlBar.el().querySelector(".vjs-live-control");if(t)f=A.controlBar.el().querySelector(".vjs-seek-to-live-control");i.style.background="transparent";f.parentNode.insertBefore(i,f.nextSibling)}else if("overlay"===e.position){i.style.position="absolute";i.style.top=e.offsetV+"px";i.style.fontSize=e.fontSize+"px";i.style.background=e.background;i.style.color="#fff";i.style.lineHeight="24px";i.style.padding="2px 6px";i.style.borderRadius="4px";i.style.height="auto";if("right"===e.direction)i.style.right=e.offsetH+"px";else if("left"===e.direction)i.style.left=e.offsetH+"px";A.el().appendChild(i)}}s();o=setTimeout(c,1e3)}}},liveclock=function(A){this.ready(function(){try{onPlayerReady(this,videojs.obj.merge(defaults,A))}catch(e){onPlayerReady(this,videojs.mergeOptions(defaults,A))}})};if("undefined"!==typeof window)videojs.registerPlugin("liveclock",liveclock);liveclock.VERSION="1.0";export default liveclock;
7 |
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/morevideo.css:
--------------------------------------------------------------------------------
1 | .video-js .vjs-more-button {
2 | position: absolute;
3 | left: 2%;
4 |
5 | cursor:pointer;
6 | color:#fff;
7 | font-size:14px;
8 | font-family:Arial,sans-serif;
9 | font-weight:normal;
10 | font-style:normal;
11 | text-decoration:none;
12 | text-shadow: 1px 1px 1px #000;
13 | }
14 |
15 | .video-js .vjs-more-button .svg {
16 | width: 16px;
17 | height: 16px;
18 | fill: #fff;
19 | display: inline-block;
20 | float: left;
21 | margin-right: 5px;
22 | filter: drop-shadow(1px 1px 1px #000);
23 | }
24 |
25 | .video-js .vjs-more-video {
26 | width: 100%;
27 | position:absolute;
28 | left: 0;
29 | z-index:97;
30 | background:rgba(0,0,0,.33);
31 | opacity:1;
32 | pointer-events:auto;
33 | display:block;
34 | transition:top .5s ease, opacity .5s ease;
35 | }
36 | .video-js .vjs-more-hidden {
37 | opacity:0;
38 | pointer-events:none;
39 | }
40 |
41 | .video-js .vjs-more-header {
42 | text-align:left;
43 | width:100%;
44 | left:1.5%;
45 | font-size:14px;
46 | text-shadow:1px 1px 1px #000;
47 | font-weight:normal;
48 | padding: 5px 1.5%;
49 | padding-top:5px;
50 | }
51 | .video-js .vjs-more-header span {
52 | cursor:pointer;
53 | }
54 |
55 | .video-js .vjs-more-close{
56 | float:right;
57 | cursor:pointer;
58 | padding: 0;
59 | margin-top:-8px;
60 | -webkit-transition: all .3s;
61 | transition: all .3s;
62 | }
63 | .video-js .vjs-more-close:after {
64 | display: inline-block;
65 | content: "\00d7";
66 | font-size:30px;
67 | }
68 | .video-js .vjs-more-close:hover {
69 | -webkit-transform:scale(1.3);
70 | transform:scale(1.3);
71 | }
72 |
73 |
74 | .video-js .vjs-more-inside {
75 | width: 100%;
76 | position:relative;
77 | background-color:transparent;
78 | text-align:center;
79 | }
80 | .video-js .vjs-more-line {
81 | width:97%;
82 | left:1.5%;
83 | bottom:8px;
84 | position:absolute;
85 | }
86 | .vjs-more-video .vjs-more-arrow {
87 | position: absolute;
88 | cursor:pointer;
89 | display:table;
90 | width:27px;
91 | }
92 | .vjs-more-video .vjs-disabled {
93 | opacity: .35;
94 | -moz-opacity: 0.35;
95 | -khtml-opacity: 0.35;
96 | cursor:none;
97 | }
98 |
99 |
100 | .vjs-more-video .vjs-more-arrow-prev, .vjs-more-video .vjs-more-arrow-next {
101 | z-index: 10;
102 | cursor: pointer;
103 | display: block;
104 | margin: 0 auto;
105 | width: 35px;
106 | height: 35px;
107 | border-top: 2px solid #fff;
108 | border-left: 2px solid #fff;
109 |
110 | }
111 | .vjs-more-video .vjs-more-arrow-next {
112 | right:10px;
113 | transform: rotate(135deg) translateY(-50%);
114 | top: 50%;
115 | transform-origin: top center;
116 | }
117 |
118 | .vjs-more-video .vjs-more-arrow-prev {
119 | transform: rotate(-45deg) translateY(-50%);
120 | margin-left: 7px;
121 | top: 50%;
122 | transform-origin: top center;
123 | }
124 |
125 |
126 | .video-js .vjs-more-video .vjs-more-list {
127 | position:absolute;
128 | overflow:hidden;
129 | top:0;
130 | width:100%;
131 | height:100%;
132 | }
133 |
134 | .video-js .vjs-more-video .more-block {
135 | position: absolute;
136 | left: 0;
137 | top: 0;
138 | height:100%;
139 | transition: left 0.5s ease;
140 | -webkit-transition: left 0.5s ease;
141 | -mos-transition: left 0.5s ease;
142 | }
143 |
144 | .video-js .more-anim-touch {
145 | position:relative;
146 | overflow-x:scroll!important;
147 | overflow-y:hidden!important;
148 | -webkit-overflow-scrolling:touch!important;
149 | white-space:nowrap;
150 | }
151 | .video-js .vjs-more-video .more-animo-touch {
152 | transition: left 0.5s ease-out;
153 | -webkit-transition: left 0.5s ease-out;
154 | -mos-transition: left 0.5s ease-out;
155 | }
156 | .video-js .vjs-more-video .vjs-more-list .more-item-parent {
157 | position: absolute; padding:2px;height:100%;
158 | }
159 | .video-js .vjs-more-video .more-item {
160 | width:100%;
161 | height:100%;
162 |
163 | }
164 | .video-js .vjs-more-video .more-item:hover .rel-bg{
165 | opacity:0.7;
166 | }
167 | .video-js .vjs-more-video .more-item:hover label, .video-js .vjs-more-video .more-item:hover i {
168 | opacity:1;
169 | }
170 |
171 | .video-js .vjs-more-video .more-item a {
172 | width:100%;height:100%;display:block;
173 | background-color:#000;
174 | }
175 | .video-js .vjs-more-video .more-item-bg {
176 | width:100%;
177 | height:100%;
178 | background-size:cover;
179 | background-position: center;
180 | transition: opacity 0.3s ease;
181 | -webkit-transition: opacity 0.3s ease;
182 | -moz-transition: opacity 0.3s ease;
183 | opacity:1;
184 | display:block;
185 | }
186 | .video-js .vjs-more-video label {
187 | width: 100%;
188 | min-width:100%;
189 | max-width:100%;
190 | margin:0;
191 | text-align: left;
192 | color: #fff;
193 | font-size: 13px;
194 | white-space:nowrap;
195 | font-weight:normal;
196 | font-family:sans-serif;
197 | line-height:20px;
198 | max-height: 44px;
199 | overflow: hidden;
200 | text-overflow: ellipsis;
201 | text-shadow: 1px 1px 1px #000;
202 | box-sizing: border-box;
203 | position: absolute;
204 | top: 0;
205 | left: 0;
206 | padding: 3% 5%;
207 | cursor: pointer;
208 | transition: opacity 1s ease;
209 | -webkit-transition: opacity 1 ease;
210 | -mos-transition: opacity 1 ease;
211 | opacity:0;
212 | }
213 | .video-js .vjs-more-video i {
214 | position: absolute;
215 | color: #fff;
216 | bottom: 0;
217 | right: 0;
218 | padding: 4px;
219 | font-style: normal;
220 | background:rgba(0,0,0,0.5);
221 | margin:2px;
222 | font-size:12px;
223 | transition: opacity 1s ease;
224 | -webkit-transition: opacity 1s ease;
225 | -mos-transition: opacity 1s ease;
226 | opacity:0;
227 | }
228 | .vjs-more-touch .vjs-more-video label, .vjs-more-touch .vjs-more-video i {
229 | opacity:1;
230 | }
231 |
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/morevideo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 The Nuevodevel Team. All rights reserved.
3 | * Morevideo plugin for video.js
4 | * Version 2.5.0
5 | */
6 | /* eslint-disable */import videojs from"video.js";const onPlayerReady=function(p,f,e){const h={onpause:!1,oneshow:!1,target:"_blank"};e&&e.onpause&&(h.onpause=!0);function u(e,s,t){e=document.createElement(e);"undefined"!==typeof s&&""!==s&&(e.className=s);"undefined"!==typeof t&&""!==t&&(e.innerHTML=t);return e}if(Array.isArray(f)&&!(f.length<2)){var m=videojs.dom,j=p.el(),y=0,x=0,C=1,g=1,b=0,k=null,w=null,q=null,S=null,M=!1,_=null,z=null,W=null,I=null,H=null;p.on("playerresize",function(){P()});p.one("playing",function(){if(null===z){var s=u("div","vjs-more-button"),e=(s.innerHTML=''+p.localize("More videos"),j.querySelector(".vjs-control-bar"));h.onpause||e.insertBefore(s,e.firstChild);z=u("div","vjs-more-video vjs-more-hidden");p.on("play",()=>{});s.onclick=function(e){e.preventDefault();e.stopImmediatePropagation();n(!1)};p.on("pause",function(){h.onpause&&!h.oneshow&&n()});p.on("play",function(){h.onpause&&d()});s.addEventListener("touchstart",function(e){e.stopImmediatePropagation();n(!0)},{passive:!0});var t=u("div","vjs-more-header"),o=(t.innerHTML=""+p.localize("More videos")+"...",u("div","vjs-more-close"));t.appendChild(o);z.appendChild(t);H=u("div","vjs-more-inside");(I=u("div","vjs-more-line")).innerHTML='';w=I.querySelector(".vjs-more-arrow-next");k=I.querySelector(".vjs-more-arrow-prev");_=u("div","vjs-more-list");H.appendChild(I);I.appendChild(_);z.appendChild(H);e.insertBefore(z,e.firstChild);o.onclick=o.ontouchend=function(e){e.preventDefault();e.stopImmediatePropagation();h.oneshow=!0;d()};t=.97*p.controlBar.el_.offsetWidth,e=t-80,o=(_.style.left="40px",6);t<1020&&(o=5);t<830&&(o=4);t<600&&(o=3);b=o=t<400?2:o;g=1;var i=parseInt(e/o,10),t=parseInt(.5625*i,10);_.style.maxWidth=i*o+"px";I.style.height=t+"px";H.style.height=t+16+"px";W=u("div","more-block");_.appendChild(W);y=f.length;x=Math.ceil(y/o);for(var l=0;l"+f[l].duration+""}q=I.querySelector(".vjs-more-arrow-next");S=I.querySelector(".vjs-more-arrow-prev");L(!0);q.onclick=function(e){e.stopImmediatePropagation();v()};S.onclick=function(e){e.stopImmediatePropagation();c()}}function n(e){m.removeClass(z,"vjs-more-hidden");e&&m.addClass(_,"more-anim-touch");M=e;P();L();m.removeClass(j,"vjs-more-touch");m.addClass(s,"vjs-hidden")}function d(){m.addClass(z,"vjs-more-hidden");m.removeClass(s,"vjs-hidden");m.removeClass(j,"vjs-more-touch");z.style.top="-5px"}function v(){if(!(w.className.indexOf("vjs-disabled")>-1)){var e=.97*j.offsetWidth,s=6;e<1020&&(s=5);e<830&&(s=4);e<600&&(s=3);b=s=e<400?2:s;e=_.offsetWidth,s=_.querySelector(".vjs-more-first").offsetWidth*y,s=(x=Math.ceil(y/b),s-e);if((C=Math.ceil(g/b))===x)m.addClass(w,"vjs-disabled");else{g=b*(++C-1)+1;e=(C-1)*e;e>s&&(e=s);z.querySelector(".more-block").style.left="-"+e+"px";C===x&&m.addClass(w,"vjs-disabled")}m.removeClass(k,"vjs-disabled")}}function c(){if(!(k.className.indexOf("vjs-disabled")>-1)){var e=_.offsetWidth,s=.97*j.offsetWidth,t=6;s<1020&&(t=5);s<830&&(t=4);s<600&&(t=3);b=t=s<400?2:t;x=Math.ceil(y/b);C=Math.ceil(g/b);if(1!==C){g=b*((C-=1)-1)+1;s=(C-1)*e;z.querySelector(".more-block").style.left="-"+s+"px";1===C&&m.addClass(k,"vjs-disabled");m.removeClass(w,"vjs-disabled")}}}});return this}function P(){if(null!==z){var e=.97*p.controlBar.el_.offsetWidth,s=e;if(M)_.style.left=0;else{s=e-80;_.style.left="40px"}var t=6;e<1020&&(t=5);e<830&&(t=4);e<600&&(t=3);b=t=e<400?2:t;var o=parseInt(s/t,10),e=(_.style.maxWidth=o*t+"px",parseInt(.5625*o,10));if(C>x){C=x;m.addClass(w,"vjs-disabled")}else m.removeClass(w,"vjs-disabled");var t=_.offsetWidth,t=o*y-t,i=(g-1)*o;W.style.left="-"+(i=i>t?t:i)+"px";I.style.height=e+"px";H.style.height=e+16+"px";for(var l=0,r=W.children,a=0;a1080&&(t=80);if(p.el_.querySelector(".vjs-skin-shaka ")&&i>1080){s=40;o+=10;t=60}if(p.el_.querySelector(".vjs-skin-treso")){t=65;i>1080&&(t=80);if(i>1080){s=60;o+=30}else{s=50;o+=15}}if(p.el_.querySelector(".vjs-skin-roundal")){s+=5;o+=5;t=55}if(p.el_.querySelector(".vjs-skin-nuevo")){s=35;t=55}p.el_.querySelector(".vjs-skin-flow")&&(t=50);if(p.el_.querySelector(".vjs-skin-party")){t=50;if(i>1080){s=40;o+=10}else{s=30;o+=5}}if(p.el_.querySelector(".vjs-skin-mockup")){t=75;if(i>1080){s=80;o+=50}else{s=60;o+=30}}if(p.el_.querySelector(".vjs-skin-pinko")){s+=5;o+=5;t=50}if(p.el_.querySelector(".vjs-skin-jwlike"))if(i>1080){s=45;o+=15}else{s=35;o+=5}if(p.el_.querySelector(".vjs-skin-chrome")){t=50;s=40;o+=10}z.style.top=0;var l=j.querySelector(".vjs-more-button");l&&(l.style.top="-"+s+"px");if(h.onpause){i>1080&&(t=80);z.style.top="auto";z.style.bottom=t+"px"}else!0!==e&&(z.style.top="-"+parseInt(o,10)+"px")}},morevideo=function(e,s){this.ready(()=>{onPlayerReady(this,e,s)})};if("undefined"!==typeof window){const Ea=videojs.registerPlugin||videojs.plugin;Ea("morevideo",morevideo)}export default morevideo;
7 |
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/nuevo.d.ts:
--------------------------------------------------------------------------------
1 | import { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js';
2 |
3 | declare global {
4 | interface Window {
5 | videojs: any;
6 | }
7 | interface Window {
8 | nuevoskin: string;
9 | }
10 | }
11 |
12 | declare module 'video.js' {
13 | export interface VideoJsPlayer {
14 | nuevo(options: any): void;
15 | loadTracks(tracks?: any): void;
16 | video_id(): string;
17 | video_title(): string;
18 | setRate(rate?: any): void;
19 | resetNuevo(how?: boolean): void;
20 | setSource(item?: any): void;
21 | changeSource(item?: any): void;
22 | changeSrc(item?: any): void;
23 | textTracksStyle(style?: any): void;
24 | setQuality(level?: any, toggle?: boolean): void;
25 | vroll(list?: any): void;
26 | thumbnails(thumbnails?: any): void;
27 | playlist: any;
28 | upnext: any;
29 | liveclock: any;
30 | filters: any;
31 | events: any;
32 | hotkeys: any;
33 | visualizer(options?: any): void;
34 | offline: any;
35 | hlsjs: any;
36 | ima: any;
37 | vastAds: any;
38 | airplay: any;
39 | dai: any;
40 | chromecast: any;
41 | vast: any;
42 | ticker: any;
43 | youtube: any;
44 | id3: any;
45 | }
46 | }
47 |
48 | export interface nuevoOptions {
49 | logo?: string;
50 | logoposition: string;
51 | logooffsetX: number;
52 | logooffsetY: number;
53 | logourl: string;
54 | target: string;
55 | zoomMenu: boolean;
56 | relatedMenu: boolean;
57 | rateMenu: boolean;
58 | shareMenu: boolean;
59 | qualityMenu: boolean;
60 | filtersMenu: boolean;
61 | contextMenu: boolean;
62 | contextLink: boolean;
63 | pipButton: boolean;
64 | ccButton: boolean;
65 | settingsButton: boolean;
66 | downloadButton: boolean;
67 | buttonRewind: boolean;
68 | buttonForward: boolean;
69 | rewindforward: number;
70 | video_id: string;
71 | url: string;
72 | title: string;
73 | description: string;
74 | embed: string;
75 | endAction: string;
76 | pubid: string;
77 | slideWidth: number;
78 | slideHeight: number;
79 | slideType: string;
80 | currentSlide: string;
81 | chapterMarkers: boolean;
82 | rate: number;
83 | resume: boolean;
84 | infoSize: number;
85 | infoIcon: string;
86 | hdicon: boolean;
87 | zoomInfo: boolean;
88 | timetooltip: boolean;
89 | captionsSettings: any;
90 | mousedisplay: boolean;
91 | related: any;
92 | limit: number;
93 | limitmessage: string;
94 | playlistID: string;
95 | playlistMaxH: number;
96 | playlistUI: boolean;
97 | playlistShow: boolean;
98 | playlistAutoHide: boolean;
99 | playlist: boolean;
100 | metatitle: string;
101 | metasubtitle: string;
102 | tooltips: boolean;
103 | singlePlay: boolean;
104 | snapshot: boolean;
105 | snapshotType: string;
106 | snapshotWatermark: string;
107 | ghostThumb: false;
108 | minhd: number;
109 | liveReconnect: boolean;
110 | paused: boolean;
111 | controlbar: boolean;
112 | touchRewindForward: boolean;
113 | touchControls: boolean;
114 | iosFullscreen: string;
115 | androidLock: boolean;
116 | playsinline: boolean;
117 | keepSource: boolean;
118 | log: boolean;
119 | }
120 |
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/upnext.css:
--------------------------------------------------------------------------------
1 | .video-js .vjs-nextup svg {
2 | position: relative;
3 | width: 76px;
4 | height: 76px;
5 | z-index: 1000;
6 | }
7 | .video-js .vjs-nextup .next-close {
8 | position: absolute;
9 | right: -10px;
10 | top: -5px;
11 | font-family: sans-serif;
12 | font-size: 24px;
13 | color: #fff;
14 | cursor: pointer;
15 | }
16 |
17 | .video-js .vjs-nextup svg .circle1,
18 | .video-js .vjs-nextup svg .circle2 {
19 | width: 100%;
20 | height: 100%;
21 | fill: none;
22 | stroke-width: 2;
23 | stroke-linecap: round;
24 | transform: translate(2px, 2px);
25 | stroke-linejoin: round;
26 | }
27 | .video-js .vjs-nextup svg .circle1 {
28 | fill: rgba(0, 0, 0, 0.2);
29 | }
30 | .video-js .vjs-nextup svg .circle2 {
31 | stroke: #fff;
32 | }
33 |
34 | .video-js .vjs-nextup .progress {
35 | position: absolute;
36 | top: 50%;
37 | left: 50%;
38 | transform: translate(-50%, -50%) rotate(-90.01deg);
39 | width: 76px;
40 | height: 76px;
41 | border-radius: 50%;
42 | z-index: 1000;
43 | pointer-events: none;
44 | }
45 |
46 | .vjs-640 .vjs-nextup svg,
47 | .vjs-480 .vjs-nextup svg,
48 | .vjs-640 .vjs-nextup .progress,
49 | .vjs-480 .vjs-nextup .progress {
50 | width: 60px;
51 | height: 60px;
52 | }
53 |
54 | .video-js .next-overlay {
55 | width: 100%;
56 | height: 100%;
57 | position: absolute;
58 | top: 0;
59 | left: 0;
60 | background: #2b4162;
61 | background-image: linear-gradient(325deg, #2b4162 0%, #12100e 74%);
62 | }
63 | .video-js .vjs-nextup {
64 | position: absolute;
65 | top: 50%;
66 | left: 50%;
67 | width: 50%;
68 | max-width: 640px;
69 | -webkit-transform: translate(-50%, -50%);
70 | transform: translate(-50%, -50%);
71 | z-index: 99;
72 | }
73 | .video-js .vjs-nextup .next-header {
74 | font-size: 22px;
75 | margin-bottom: 10px;
76 | text-align: center;
77 | color: #999;
78 | }
79 | .video-js .vjs-nextup .next-header span {
80 | background: -webkit-linear-gradient(#fff, #333);
81 | -webkit-background-clip: text;
82 | -webkit-text-fill-color: transparent;
83 | }
84 |
85 | .video-js .vjs-fullnext {
86 | position: relative;
87 | cursor: pointer;
88 | overflow: hidden;
89 | border-radius: 1em;
90 | }
91 |
92 | .video-js .next-overlay .vjs-nav-prev,
93 | .video-js .next-overlay .vjs-nav-next {
94 | position: absolute;
95 | top: 55%;
96 | -webkit-transform: translateY(-55%);
97 | transform: translateY(-55%);
98 | cursor: pointer;
99 | display: table;
100 | padding: 0;
101 | }
102 | .video-js .next-overlay .vjs-nav-prev {
103 | left: 20%;
104 | }
105 | .vjs-480 .next-overlay .vjs-nav-prev {
106 | left: 5%;
107 | }
108 | .video-js .next-overlay .vjs-nav-next {
109 | right: 20%;
110 | }
111 | .vjs-480 .next-overlay .vjs-nav-next {
112 | right: 5%;
113 | }
114 | .video-js .next-overlay .vjs-nav-prev .icon,
115 | .video-js .next-overlay .vjs-nav-next .icon {
116 | width: 27px;
117 | height: 27px;
118 | z-index: 10;
119 | cursor: pointer;
120 | border: solid white;
121 | border-width: 0 3px 3px 0;
122 | display: inline-block;
123 | padding: 3px;
124 | }
125 |
126 | .video-js .next-overlay .vjs-nav-prev .icon {
127 | -webkit-transform: rotate(135deg);
128 | transform: rotate(135deg);
129 | }
130 | .video-js .next-overlay .vjs-nav-next .icon {
131 | -webkit-transform: rotate(-45deg);
132 | transform: rotate(-45deg);
133 | }
134 | .video-js .next-overlay .disabled {
135 | opacity: 0.35;
136 | cursor: none;
137 | }
138 | .video-js .vjs-fullnext .respo {
139 | width: 100%;
140 | position: relative;
141 | padding-top: 56.25%;
142 | height: auto;
143 | background: #000;
144 | }
145 | .video-js .vjs-fullnext .img {
146 | position: absolute;
147 | top: 0;
148 | left: 0;
149 | width: 100%;
150 | height: 100%;
151 | background-size: cover;
152 | background-repeat: no-repeat;
153 | background-position: center;
154 | }
155 |
156 | .vjs-480 .vjs-nextup {
157 | width: 65%;
158 | }
159 | .vjs-480 .vjs-nextup .next-header {
160 | font-size: 18px;
161 | }
162 |
163 | .video-js .vjs-fullnext .full-dur {
164 | position: absolute;
165 | top: 0;
166 | left: 0;
167 | padding: 5px;
168 | background: rgba(0, 0, 0, 0.5);
169 | border-bottom-right-radius: 5px;
170 | color: #ccc;
171 | }
172 |
173 | .video-js .vjs-fullnext .next-title {
174 | padding: 0 10px 10px 10px;
175 | color: #fff;
176 | font-size: 16px;
177 | background: linear-gradient(0deg, rgba(0, 0, 0, 0.6), transparent);
178 | position: absolute;
179 | text-align: left;
180 | bottom: 0;
181 | left: 0;
182 | width: 100%;
183 | text-shadow: 1px 1px 1px #000;
184 | }
185 | .vjs-480 .vjs-fullnext .next-title {
186 | font-size: 14px;
187 | }
188 |
189 | .video-js .vjs-fullnext img {
190 | width: 100%;
191 | display: block;
192 | }
193 |
194 | .video-js .vjs-upnext {
195 | position: absolute;
196 | z-index: 12;
197 | background: #000;
198 | right: -100%;
199 | bottom: 0;
200 | margin: 0;
201 | padding: 0;
202 | height: 60px;
203 | overflow: hidden;
204 | font-weight: normal;
205 | font-family: Arial, sans-serif;
206 | font-size: 14px;
207 | cursor: pointer;
208 | border: solid 1px #444;
209 | -webkit-transition: all 0.5s ease-in-out;
210 | transition: all 0.5s ease-in-out;
211 | }
212 | .vjs-user-active .vjs-upnext {
213 | bottom: 65px;
214 | }
215 | .vjs-1600.vjs-user-active .vjs-upnext {
216 | bottom: 100px;
217 | }
218 | .vjs-paused:not(.vjs-touch-inactive) .vjs-upnext {
219 | bottom: 65px;
220 | }
221 | .vjs-1600.vjs-paused:not(vjs-touch-inactive) .vjs-upnext {
222 | bottom: 100px;
223 | }
224 |
225 | .vjs-touchactive .vjs-upnext {
226 | bottom: 65px;
227 | }
228 | .vjs-1600.vjs-touchactive .vjs-upnext {
229 | bottom: 90px;
230 | }
231 |
232 | .video-js .vjs-upnext-show {
233 | right: 0;
234 | }
235 | .video-js .vjs-upnext img {
236 | border: 0;
237 | float: left;
238 | height: 60px;
239 | margin: 0 10px 0 0;
240 | padding: 0;
241 | }
242 | .video-js .vjs-upnext .next-dur {
243 | position: absolute;
244 | font-size: 12px;
245 | font-family: Arial, sans-serif;
246 | color: #fff;
247 | padding: 0 2px;
248 | background: rgba(0, 0, 0, 0.75);
249 | bottom: 0;
250 | color: #fff;
251 | }
252 | .video-js .vjs-upnext .upnext-right {
253 | display: inline-block;
254 | vertical-align: top;
255 | padding: 0 8px 0 0;
256 | max-width: 170px;
257 | margin: 0;
258 | }
259 | .video-js .vjs-upnext .nextup {
260 | margin: 0;
261 | padding: 4px 0 5px 0;
262 | font-weight: bold;
263 | font-family: Arial, sans-serif;
264 | display: block;
265 | color: #fff;
266 | }
267 | .video-js .vjs-upnext span {
268 | font-size: 12px;
269 | color: #ccc;
270 | padding: 0;
271 | font-family: Arial, sans-serif;
272 | font-weight: normal;
273 | }
274 | .video-js .vjs-upnext .vjs-up-title {
275 | display: -webkit-box;
276 | -webkit-line-clamp: 2;
277 | -webkit-box-orient: vertical;
278 | overflow: hidden;
279 | text-align:left;
280 | max-height: 30px;
281 | }
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/upnext.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2023 The Nuevodevel Team. All rights reserved.
3 | * UpNext plugin for video.js
4 | * Version 1.2.0
5 | */
6 | /* eslint-disable */import videojs from"video.js";function factory(e,i,l){var n={nextTitle:"Up Next",offset:30};try{l=videojs.obj.merge(n,l||{})}catch(e){l=videojs.mergeOptions(n,l||{})}var t,a,o,r=!1,d=!1,s=l.nextTitle,c=l.offset,v=0;e.upnext.title=function(e){e.length>1&&(s=e)};e.upnext.offset=function(e){parseInt(e,10)>1&&(c=e)};if(!Array.isArray(i)&&i.title&&(i.sources||i.url)&&i.poster){var u=[];u[0]=i;i=u}if(Array.isArray(i)){for(var p=[],f=0;f1){t=p[1];v=1}else t=p[0];var m=null,g=null,j=null,C=null;if(0!=p.length){videojs.dom.addClass(e.el(),"vjs-up-next");e.on("timeupdate",function(){if(!videojs.dom.hasClass(e.el(),"vjs-ad-playing")&&!videojs.dom.hasClass(e.el(),"vjs-dai")&&!(p.length<1)&&!(p.length>1&&v==p.length-1)&&!x()){!0!==r&&e.currentTime()>0&&e.duration()-e.currentTime()c){if(m){m.onclick=null;m.parentNode.removeChild(m);m=null}r=!1}}});e.on("ended",function(){videojs.dom.hasClass(e.el(),"vjs-ad-playing")||videojs.dom.hasClass(e.el(),"vjs-dai")||k()});return this}}else console.log("Error: Upnext - Invalid list array");function x(){return e.duration()===1/0||"8"===videojs.browser.IOS_VERSION&&0===e.duration()}function y(e,i,l){var n=document.createElement(e);i&&(n.className=i);l&&(n.innerHTML=l);return n}function b(){if(!r&&!d){(m=y("div","vjs-upnext")).ariaDisabled=!1;m.tabIndex="0";m.ariaLabel=e.localize("UpNext video");var i=t.img;i.setAttribute("alt",t.title);m.appendChild(i);if(t.duration){var l=y("div","next-dur",t.duration);m.appendChild(l)}var n=y("span","upnext-right",''+s+''+t.title+"");m.appendChild(n);if(t.url){var a=y("a");a.href=t.url;a.target="_blank";t.target?a.target=t.target:a.target="_blank";m.appendChild(a)}e.el().appendChild(m);setTimeout(()=>{m.className="vjs-upnext vjs-upnext-show"},500);r=!0;m.onkeydown=function(e){if(13===e.which){e.preventDefault();e.stopPropagation();m.click()}};m.onclick=function(){if(a)a.click();else{e.changeSource(t);m.onclick=null;e.el().removeChild(m);m=null;e.play();if(1==p.length){d=!0;videojs.dom.removeClass(e.el(),"vjs-up-next");p.splice(0,1)}else v"+e.localize("Up Next")+"");j.appendChild(l);g=y("div","vjs-fullnext");j.appendChild(g);var n=y("div","respo");n.tabIndex=0;n.ariaDiabled=!1;n.ariaLabel=e.localize("Play")+" "+t.title;(a=y("div","img")).style.backgroundImage="url("+t.poster+")";n.appendChild(a);g.appendChild(n);if(t.duration){var r=y("div","full-dur",t.duration);g.appendChild(r)}var s=y("div","next-title",t.title);g.appendChild(s);var c=y("div","progress");c.innerHTML='';g.appendChild(c);n.onclick=function(){if(t.url){(o=y("a")).href=t.url;t.target?o.target=t.target:o.target="_blank";g.appendChild(o);o.click()}else N()};C=y("div","next-overlay");e.el().appendChild(C);e.el().appendChild(j);if(p.length>1){var u="vjs-nav-prev",f="vjs-nav-next";0===v&&(u="vjs-nav-prev disabled");v===p.length-1&&(f="vjs-nav-next disabled");var h=y("div",u,''),x=y("div",f,'');h.ariaDisabled=x.ariaDisabled=!1;h.tabIndex=x.tabIndex=0;h.role=x.role="button";x.ariaLabel=e.localize("Next video");h.ariaLabel=e.localize("Previous video");C.appendChild(h);C.appendChild(x);x.onkeydown=function(e){if(13===e.which){e.preventDefault();e.stopPropagation();x.click()}};x.onclick=function(){if(++v>p.length-1)v=p.length-1;else{v===p.length-1&&videojs.dom.addClass(x,"disabled");videojs.dom.removeClass(h,"disabled");k(v,"next")}};h.onkeydown=function(e){if(13===e.which){e.preventDefault();e.stopPropagation();h.click()}};h.onclick=function(){if(--v<0)v=0;else{0===v&&videojs.dom.addClass(h,"disabled");videojs.dom.removeClass(x,"disabled");k(v,"prev")}};var b=y("div","next-close");b.innerHTML="ⓧ";b.tabIndex="0";b.ariaLabel=e.localize("Close UpNext container");b.ariaDisabled=!1;j.appendChild(b);b.onkeydown=function(e){if(13==e.which){e.preventDefault();e.stopPropagation();b.click()}};b.onclick=function(){g.onclick=null;if(j){j.parentNode.removeChild(j);j=null}if(C){C.parentNode.removeChild(C);C=null}e.play();e.one("playing",function(i){videojs.dom.removeClass(e.el(),"vjs-up-next");videojs.dom.removeClass(e.controlBar.el_,"vjs-hidden");d=!0;if(1==p.length){d=!0;p.splice(0,1);t=null}})};function k(i,l){t=p[i];var d=y("div","img");d.style.backgroundImage="url("+p[i].poster+")";d.style.left="next"==l?"100%":"-100%";var c,v=100,u=-100;n.appendChild(d);function f(){if((v-=3)<=0){cancelAnimationFrame(c);d.style.left=0;a.parentNode.removeChild(a);a=d;d=null}else{v>0&&(d.style.left=v+"%");c=requestAnimationFrame(f)}}function h(){if((u+=3)>=0){cancelAnimationFrame(c);d.style.left=0;a.parentNode.removeChild(a);a=d;d=null}else{u<0&&(d.style.left=u+"%");c=requestAnimationFrame(h)}}c="next"==l?requestAnimationFrame(f):requestAnimationFrame(h);s.innerHTML=p[i].title;n.ariaLabel=e.localize("Play")+" "+p[i].title;o&&n.removeChild(o);if(p[i].duration&&r){r.innerHTML=p[i].duration;videojs.dom.removeClass(r,"vjs-hidden")}else r&&videojs.dom.addClass(r,"vjs-hidden")}}}function N(){g.onclick=null;if(j){j.parentNode.removeChild(j);j=null}if(C){C.parentNode.removeChild(C);C=null}e.changeSource(t);e.play();e.one("playing",function(i){videojs.dom.removeClass(e.el(),"vjs-up-next");videojs.dom.removeClass(e.controlBar.el_,"vjs-hidden");if(1==p.length){d=!0;p.splice(0,1);t=null}});p.length>1&&ve.playbackRate()){e.setRate(q[P]);break}}switch(E(i,e)){case a:if(t.enableCC){var R=e.el_.querySelector(".vjs-subs-caps-button");if(R){var D=R.querySelectorAll("li");if(D.length>1)if(S>0){D[S].click();S=0;e.el_.focus();break}else{var C=R.querySelector(".vjs-selected");if(C.classList.contains("vjs-captions-menu-item"))F=C;if(null!==F){for(P=0;P47&&g<59||g>95&&g<106)if(k||!(i.metaKey||i.ctrlKey||i.altKey))if(b){var H=48;if(g>95)H=96;var x=g-H;i.preventDefault();e.currentTime(e.duration()*x*.1)}for(var A in t.customKeys){var I=t.customKeys[A];if(I&&I.key&&I.handler)if(I.key(i)){i.preventDefault();I.handler(e,t)}}}}}}},q=function(t){if(e.controls()){var i=t.relatedTarget||t.toElement||r.activeElement;if(i===n||i===n.querySelector(".vjs-tech")||i===n.querySelector(".iframeblocker"))if(v)if(e.isFullscreen())e.exitFullscreen();else e.requestFullscreen()}},E=function(e,n){if(t.playPauseKey(e,n))return l;if(t.muteKey(e,n))return s;if(t.captionsKey(e,n))return a;if(t.fullscreenKey(e,n))return u;else return};function P(e){return 32===e.which||179===e.which}function R(e){return 67===e.which}function D(e){return 77===e.which}function C(e){return 70===e.which}function H(){o=!1}document.onkeydown=T;document.onkeyup=H;e.on("keydown",T);e.on("keyup",H);e.on("dblclick",q)}}if("undefined"!=typeof window)videojs.registerPlugin("hotkeys",Hotkeys);export default Hotkeys;
7 |
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/videojs.offline.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 The Nuevodevel Team. All rights reserved.
3 | * Offline plugin for video.js
4 | * Version 2.0.0
5 | */
6 | /* eslint-disable */import videojs from"video.js";var defaults={offlineImage:"",offlineTimeout:30,liveTyle:"",offlineCountdown:!1,clock:0,label:"restart",loadTimeout:0,resetMethod:1};try{videojs.options.vhs.maxPlaylistRetries=1}catch(e){var oe=!0}try{videojs.options.errorDisplay=!1}catch(e){oe=!0}var onPlayerReady=function(e,i){function o(e){return e.charAt(0).toUpperCase()+e.slice(1)}i.label=o(i.label);e.liveSource=function(e){i.liveSource=e};e.on("ready",function(){var o,t,n=videojs.dom,l=e.el(),r=null,f=null,s=function(e,i){try{return e.querySelector(i)}catch(e){return!1}};i.liveSource=e.currentSource();if(""!=i.offlineImage){var a=s(l,".vjs-error-display");if(a)n.addClass(a,"vjs-abs-hidden");(r=document.createElement("div")).className="vjs-poster";r.setAttribute("style",'background-position: 50% 50%;background-repeat:no-repeat; background-size: contain;top:100%;z-index:15999;opacity:1!important;pointer-events:"none";background-color:#000;');l.appendChild(r);r.style.backgroundImage="url("+i.offlineImage+")";r.style.height=0;if(i.loadTimeout>0)e.one("play",function(){t=setTimeout(function(){if(!e.isOffline){e.isOffline=!0;v()}else clearTimeout(t)},i.loadTimeout)});e.on("error",function(){if(!0===e.paused())if(!e.isOffline){var i=e.error();if(1==i.code||2==i.code||4==i.code||3==i.code||-2==i.code){e.isOffline=!0;v()}}});setInterval(function(){if(!e.isOffline){var i=l.className;if(i.indexOf("vjs-has-started")>-1)if(i.indexOf("vjs-ended")>-1){e.isOffline=!0;v()}}},500);e.on("playing",function(){r.style.height=0;r.style.display="none";e.isOffline=!1;if(!0!==videojs.browser.IS_IOS)e.muted(!1);clearTimeout(o);clearTimeout(t);try{l.removeChild(f)}catch(e){}f=null})}function c(){r.setAttribute("style",r.getAttribute("style")+";top:0;height:100%;display:block!important");if(i.offlineCountdown)if(null==f){(f=document.createElement("div")).setAttribute("style","position:absolute;right:30px;bottom:25px;font-size:20px;color:#fff;font-family:sans-serif,Arial;text-shadow:1px 1px 1px #000;z-Index:16000");l.appendChild(f)}}function u(){e.$(".vjs-tech").setAttribute("preload","auto");if(!i.liveSource)i.liveSource=e.currentSource();if(1==i.resetMethod){e.one("canplay",function(){e.play()});e.src(i.liveSource);e.load();setTimeout(function(){e.play()},100)}if(2===i.resetMethod){e.src(i.liveSource);var o=e.play();if(void 0!==o)o.then(function(){return!0}).catch(function(i){e.muted(!0);e.play()})}if(3===i.resetMethod){e.src(i.liveSource);e.load();setTimeout(function(){e.play()},100)}}function d(){c()}function p(){u()}function v(){e.isOffline=!0;var t=s(l,".vjs-loading-spinner");n.addClass(t,"vjs-abs-hidden");i.clock=0;d();function r(){clearTimeout(o);o=setTimeout(function(){i.clock++;if(i.offlineCountdown&&f)f.innerHTML=i.label+" "+y(i.offlineTimeout-i.clock);if(i.clock>=i.offlineTimeout){i.clock=0;e.trigger("offlineLoop");p()}r()},1e3)}if(i.offlineCountdown)if(null!=f)f.innerHTML=i.label+" "+y(i.offlineTimeout);i.clock=0;r()}function y(e){var i=parseInt(e,10),o=Math.floor(i/60)%60,t=i%60,n="";if(o>0)if(o>9)n=o+":";else n="0"+o+":";else n="0:";if(t>0)if(t>9)n+=t;else n+="0"+t;else n="0:00";return n}});return this},offline=function(e){this.ready(function(){try{onPlayerReady(this,videojs.obj.merge(defaults,e))}catch(i){onPlayerReady(this,videojs.mergeOptions(defaults,e))}})},registerPlugin=videojs.registerPlugin||videojs.plugin;if("undefined"!=typeof window)registerPlugin("offline",offline);offline.VERSION="3.0";export default offline;
7 |
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/videojs.thumbnails.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2023 The Nuevodevel Team. All rights reserved.
3 | * VTT Thumbnails plugin for video.js
4 | * Version 2.7.0
5 | */
6 | /* eslint-disable */import videojs from"video.js";const defaults={align:"vertical",timeTooltip:!0,width:160,height:90,basePath:"",src:"",responsive:!1,mediaqueries:{tiny:.5,small:.75,medium:1,large:1.25,xlarge:1.5}},onPlayerReady=(e,t)=>{defaults.basePath="";var i,s,r,o,n,a,d,l,h,u,c,v,f,m;if(t){if(t.basePath)defaults.basePath=t.basePath;if(t.width&&t.height){defaults.width=t.width;defaults.height=t.height}if(t.src)e.on("ready",function(){e.trigger("medialoaded",{xml:t.src})})}r=t;function p(e){var t,i,s;if(-1===(i=e.indexOf("#")))return{src:e,w:0,h:0,x:0,y:0};t=e.substring(0,i);if("xywh="!==(s=e.substring(i+1)).substring(0,5))return{src:defaults.basePath+t,w:0,h:0,x:0,y:0};var r=s.substring(5).split(",");return{src:defaults.basePath+t,w:parseInt(r[2],10),h:parseInt(r[3],10),x:parseInt(r[0],10),y:parseInt(r[1],10)}}function g(e){const t=e.split("."),i=t[0].split(":");return{milliseconds:parseInt(t[1],10)||0,seconds:parseInt(i.pop(),10)||0,minutes:parseInt(i.pop(),10)||0,hours:parseInt(i.pop(),10)||0}}function y(e){const t=g(e);return parseInt(3600*t.hours+60*t.minutes+t.seconds+t.milliseconds/1e3,10)}function w(e){fetch(e).then(e=>e.text()).then(e=>{if(e.length>0){var t=j(e);if(t.length>0){m=t;b()}}})}function j(e){var t=[];e.split(/[\r\n][\r\n]/i).forEach(function(e){if(e.match(/([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?( ?--> ?)([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?[\r\n]{1}.*/gi)){const i=e.split(/[\r\n]/i),s=i[0].split(/ ?--> ?/i),r=s[0],o=s[1],n=i[1];t.push({startTime:y(r),endTime:y(o),text:n})}});return t}e.on("medialoaded",function(t,s){m=[];d=e.controlBar.progressControl;(l=e.el_.querySelector(".vjs-progress-holder")).removeEventListener("touchstart",_);l.removeEventListener("mousemove",u);l.removeEventListener("mouseleave",v);l.removeEventListener("mousedown",S);e.sprite=!1;var r=document.querySelector(".vtt_canvas");if(r)r.parentNode.removeChild(r);var o=e.el_.querySelector(".vjs-thumb-tooltip");if(o)o.parentNode.removeChild(o);var n=document.querySelector(".vjs-thumb-image");if(n)n.parentNode.removeChild(n);var a=e.el_.querySelector(".vjs-thumbnail-holder");if(a)a.parentNode.removeChild(a);if(s&&s.xml)w(s.xml);else{d=e.controlBar.progressControl;l=e.el_.querySelector(".vjs-progress-holder");var h=e.textTracks().length;if(0===h){if(i)videojs.dom.addClass("div","vjs-hidden");return}for(var c=!1,p=0;p0||n>0?r+":":"")+(s=((r||o>=10)&&s<10?"0"+s:s)+":")+(i=i<10?"0"+i:i)}function E(){L(!1);i.classList.remove("vjs-thumb-show");if(e.shadowSlide){a.removeAttribute("style");n.width=0;n.height=0}}v=function(e){L(!1);if(!0!==videojs.holderdown)i.classList.remove("vjs-thumb-show")};function L(e){if(e)d.el().setAttribute("style","z-index:22");else d.el().removeAttribute("style")}function q(){videojs.holderdown=!1;document.removeEventListener("mousemove",u);document.removeEventListener("mouseup",q);E()}function S(e){L(!0);videojs.holderdown=!0;document.addEventListener("mousemove",u);document.addEventListener("mouseup",q);u(e)}function C(){l.removeEventListener("touchmove",u);l.removeEventListener("touchend",C);E()}function _(e){videojs.holderdown=!1;u(e);l.addEventListener("touchmove",u);l.addEventListener("touchend",C)}o=null;u=function(t){L(!0);t.preventDefault();h=e.duration();var l=d.el().querySelector(".vjs-progress-holder"),u=d.el().querySelector(".vjs-play-progress"),c=l.getBoundingClientRect(),v=null;if(t.pageX)v=t.pageX;else if(t.changedTouches)v=t.changedTouches[0].pageX||t.touches[0].clientX;var f=v-c.left;if(0===f&&videojs.holderdown&&u.offsetWidth>0);if(f<0)f=0;if(f>l.offsetWidth)f=l.offsetWidth;if(r.timeTooltip){var g=f/l.offsetWidth*h,y=i.querySelector(".vjs-thumb-tooltip");if(y)y.innerHTML=x(g,h)}for(var w=m.length,j=0,b=!1;j=g){b=!0;var q=p(E.text);break}j++}if(q){q.iw=q.w;q.ih=q.h;if(q)if(!0===b){i.classList.remove("vjs-thumb-hidden");var S=!1,C=q.src.replace(/\.\.\//g,"");if(s.src.indexOf(C)<0){s.src=q.src;S=!0}if(0===q.w){q.w=r.width;s.style.width=q.w+"px"}if(0===q.h){q.h=r.height;s.style.height=q.h+"px"}var _=1;if(r.responsive&&r.mediaqueries){var I=e.el_.offsetWidth;if(I<=320)_=r.mediaqueries.tiny;if(I>320&&I<=540)_=r.mediaqueries.small;if(I>540&&I<=1080)_=r.mediaqueries.medium;if(I>1080&&I<=1600)_=r.mediaqueries.large;if(I>1600)_=r.mediaqueries.xlarge}var T=q.w*_,N=q.h*_;if(i.style.width!==T||i.style.height!==N){i.style.width=T+"px";i.style.height=N+"px"}var P=o.getContext("2d");if(S)s.onload=function(){o.width=T;o.height=N;q.x=0;q.y=0;P.drawImage(s,q.x,q.y,q.w,q.h,0,0,o.width,o.height)};else if(q.iw>0&&q.ih>0){o.width=T;o.height=N;P.fillRect(0,0,T,N);P.drawImage(s,q.x,q.y,q.w,q.h,0,0,o.width,o.height)}var k=T/2,W=d.el().offsetWidth,M=e.el_.querySelector(".vjs-progress-holder").offsetLeft,R=k-M;if(f+k+M>W)f=W-T;else if(f{let t=[];try{t=videojs.obj.merge(defaults,e)}catch(i){t=videojs.mergeOptions(defaults,e)}onPlayerReady(this,t)})};if("undefined"!=typeof window){(videojs.registerPlugin||videojs.plugin)("thumbnails",thumbnails)}thumbnails.VERSION="2.4.0";export default thumbnails;
7 |
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/videojs.trailer.css:
--------------------------------------------------------------------------------
1 | .video-js .vjs-trailer {
2 | z-index:999998;
3 | cursor:pointer;
4 | position:absolute;
5 | max-width:320px;
6 | min-width:180px;
7 | opacity:0;
8 | width:33%;
9 | right:0;
10 | padding:0;
11 | color:#ffffff;
12 | min-height:100px;
13 | background:#000;
14 | -webkit-transition: bottom 0.3s;
15 | transition: bottom 0.3s;
16 | }
17 | .video-js .vjs-trailer-wide {
18 | width:45%;
19 | }
20 | .video-js .vjs-trail-trans {
21 | bottom: 60px;
22 | }
23 | .vjs-1600 .vjs-trail-trans {
24 | bottom: 130px;
25 | }
26 |
27 | .vjs-user-inactive.vjs-playing .vjs-trailer{
28 | bottom:0;
29 | }
30 |
31 | .video-js .vjs-trailer .vjs-vid-title {
32 | color: #fff;
33 | text-shadow: 1px 1px 1px #000;
34 | position: absolute;
35 | overflow: hidden;
36 | text-overflow:ellipsis;
37 | white-space:nowrap;
38 | padding:0 8px;
39 | top: 0;
40 | left: 0;
41 | width: 100%;
42 | height: 35px;
43 | text-align: left;
44 | font-style: italic;
45 | font-family: serif;
46 | font-size: 16px;
47 | line-height: 35px;
48 | background: linear-gradient(to top,transparent 0,rgba(0,0,0,.24) 39%,rgba(0,0,0,.48) 95%,rgba(0,0,0,.64) 100%);
49 | }
50 | .vjs-trailer-close {
51 | position: absolute;
52 | top: 5px;
53 | right: 5px;
54 | background: rgba(0,0,0,.7);
55 | text-align: center;
56 | height: 25px;
57 | width: 25px;
58 | line-height: 21px;
59 | font-size: 15px;
60 | font-family: monospace;
61 | cursor: pointer;
62 | color:white;
63 | font-style:normal;
64 | opacity:0;
65 | -webkit-border-radius:50%;
66 | border-radius:50%;
67 | }
68 | .vjs-trailer-close svg {
69 | fill:white;
70 | }
71 |
72 | .vjs-trailer:hover .vjs-trailer-close {
73 | opacity:1;
74 | }
75 | .vjs-trailer-hidden {
76 | display:none;
77 | }
78 | .video-js .vjs-trailer .vjs-watch-now {
79 | position: absolute;
80 | text-shadow: 1px 1px 1px #000;
81 | font-size: 1.1em;
82 | font-style:italic;
83 | right:10px;
84 | bottom:10px;
85 | background:#cc0000;
86 | padding:4px 5px;
87 | font-size:14px;
88 | text-decoration:none;
89 | color:#fff;
90 | outline:none;
91 | }
92 | .video-js .vjs-trailer .trailer-video-el {
93 | position:relative;
94 | color:#fff;
95 | text-decoration:none;
96 | }
97 | .video-js .vjs-trailer .trailer-video-el video {
98 | width:100%;
99 | }
100 | @media screen and (max-width: 480px) {
101 | .video-js .vjs-trailer {
102 | width:45%;
103 | }
104 | }
--------------------------------------------------------------------------------
/src/lib/videojs/plugins/es/videojs.trailer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2023 The Nuevodevel Team. All rights reserved.
3 | * Triler plugin for video.js
4 | * Version 1.5.0
5 | */
6 | /* eslint-disable */import videojs from"video.js";const onPlayerReady=(e,t)=>{var i,r={title:"",url:"",target:"",src:"",type:null,start:0,loop:!0,maxDuration:60,buttonText:"Watch Now..."};try{t=videojs.obj.merge(r,t||{})}catch(e){t=videojs.mergeOptions(r,t||{})}var a="undefined",n=0,l=0,o=!1,s=!1;if(""!==t.src){(a=document.createElement("video")).preload="auto";a.src=t.src;if(t.type)a.type=t.type;if(t.loop)a.setAttribute("loop","true");a.setAttribute("webkit-playsinline","");a.setAttribute("playsinline","true");a.setAttribute("role","application");a.muted=!0;a.load();a.onloadeddata=function(){n=a.duration};a.onerror=function(){o=!0}}else o=!0;function u(){if(e.duration()===1/0)return!0;else if("8"===videojs.browser.IOS_VERSION&&0===e.duration())return!0;return!1}e.liveTime=0;var d,c=!1;function v(){e.liveTime=e.liveTime+.5;d=setTimeout(v,500)}e.on("pause",function(){clearTimeout(d);c=!1});e.on("playing",function(){var t=e.el().className.indexOf("vjs-has-started"),i=e.el().className.indexOf("vjs-live");if(!0!==c&&t>-1&&i>-1){c=!0;d=setTimeout(v,500)}});e.on("ended",function(){if(i)e.el_.removeChild(i)});e.one("play",function(){if(o)return!1;l=parseInt(t.start,10);e.on("timeupdate",function(){var r=0;if(n>60)n=60;if(u()){if(0===l)return;r=e.liveTime}else{if(0===l)if(trailer.durationl&&!0!==s){s=!0;(i=document.createElement("div")).className="vjs-trailer vjs-trail-trans vjs-trailer-show";var o=document.createElement("div");o.className="trailer-video-el";i.appendChild(o);o.appendChild(a);if(""!==t.title){var d=document.createElement("div");d.className="vjs-vid-title";d.innerHTML=t.title;o.appendChild(d)}if(""!==t.url){var c=document.createElement("a");c.className="vjs-watch-now";c.innerHTML=t.buttonText;c.href=t.url;c.target=t.target;o.appendChild(c)}var v=document.createElement("div");v.setAttribute("role","button");v.className="vjs-trailer-close";v.innerHTML='';o.appendChild(v);e.el_.appendChild(i);var f=0;a.src=t.src;a.mute=!0;a.play();var p=setInterval(function(){if(f>.99)clearInterval(p);i.style.opacity=f;f+=.1},10);v.onclick=function(t){t.preventDefault();t.stopImmediatePropagation();if(i)e.el_.removeChild(i)};if(t.maxDuration>0)if(i)a.ontimeupdate=function(){if(a.currentTime>t.maxDuration){a.pause();if(i)e.el_.removeChild(i);i=null}};a.onended=function(){if(!0!==t.loop){var r=i;r.style.opacity=1;var a=1,n=setInterval(function(){if(a<.1){clearInterval(n);if(r)e.el_.removeChild(r)}if(r)r.style.opacity=a;a-=.1},30)}}}})});return this},trailer=function(e){this.ready(()=>{onPlayerReady(this,e)})};var registerPlugin=videojs.registerPlugin||videojs.plugin;registerPlugin("trailer",trailer);export default trailer;
7 |
--------------------------------------------------------------------------------
/src/lib/videojs/skins/treso/upnext.css:
--------------------------------------------------------------------------------
1 | .video-js .vjs-nextup svg {
2 | position: relative;
3 | width: 76px;
4 | height: 76px;
5 | z-index: 1000;
6 | }
7 | .video-js .vjs-nextup .next-close {
8 | position:absolute;
9 | right:-10px;
10 | top:-5px;
11 | font-family:sans-serif;
12 | font-size:24px;
13 | color:#fff;
14 | cursor:pointer;
15 | }
16 |
17 | .video-js .vjs-nextup svg .circle1, .video-js .vjs-nextup svg .circle2 {
18 | width: 100%;
19 | height: 100%;
20 | fill: none;
21 | stroke-width: 2;
22 | stroke-linecap: round;
23 | transform: translate(2px, 2px);
24 | stroke-linejoin: round;
25 | }
26 | .video-js .vjs-nextup svg .circle1 {
27 | fill:rgba(0,0,0,.2);
28 | }
29 | .video-js .vjs-nextup svg .circle2 {
30 | stroke:#fff;
31 | }
32 |
33 |
34 |
35 | .video-js .vjs-nextup .progress {
36 | position: absolute;
37 | top:50%;
38 | left:50%;
39 | transform:translate(-50%,-50%) rotate(-90.01deg);
40 | width: 76px;
41 | height: 76px;
42 | border-radius: 50%;
43 | z-index: 1000;
44 | pointer-events:none;
45 | }
46 |
47 | .vjs-640 .vjs-nextup svg,.vjs-480 .vjs-nextup svg, .vjs-640 .vjs-nextup .progress, .vjs-480 .vjs-nextup .progress {
48 | width: 60px;
49 | height: 60px;
50 | }
51 |
52 | .video-js .next-overlay {
53 | width:100%;
54 | height:100%;
55 | position:absolute;
56 | top:0;
57 | left:0;
58 | background:#2b4162;
59 | background-image:linear-gradient(325deg, #2b4162 0%, #12100e 74%);
60 | }
61 | .video-js .vjs-nextup {
62 | position: absolute;
63 | top: 50%;
64 | left: 50%;
65 | width:50%;
66 | max-width:640px;
67 | -webkit-transform: translate(-50%,-50%);
68 | transform: translate(-50%,-50%);
69 | z-index: 99;
70 | }
71 | .video-js .vjs-nextup .next-header {
72 | font-size:22px;
73 | margin-bottom:10px;
74 | text-align:center;
75 | color:#999;
76 | }
77 | .video-js .vjs-nextup .next-header span {
78 | background: -webkit-linear-gradient(#fff, #333);
79 | -webkit-background-clip: text;
80 | -webkit-text-fill-color: transparent;
81 | }
82 |
83 | .video-js .vjs-fullnext {
84 | position:relative;
85 | cursor:pointer;
86 | overflow:hidden;
87 | border-radius:1em;
88 | }
89 |
90 | .video-js .next-overlay .vjs-nav-prev, .video-js .next-overlay .vjs-nav-next {
91 | position: absolute;
92 | top: 55%;
93 | -webkit-transform: translateY(-55%);
94 | transform: translateY(-55%);
95 | cursor: pointer;
96 | display: table;
97 | padding:0;
98 | }
99 | .video-js .next-overlay .vjs-nav-prev {
100 | left:20%;
101 | }
102 | .vjs-480 .next-overlay .vjs-nav-prev {
103 | left:5%;
104 | }
105 | .video-js .next-overlay .vjs-nav-next {
106 | right:20%;
107 | }
108 | .vjs-480 .next-overlay .vjs-nav-next {
109 | right:5%;
110 | }
111 | .video-js .next-overlay .vjs-nav-prev .icon, .video-js .next-overlay .vjs-nav-next .icon {
112 | width: 27px;
113 | height: 27px;
114 | z-index: 10;
115 | cursor: pointer;
116 | border: solid white;
117 | border-width: 0 3px 3px 0;
118 | display: inline-block;
119 | padding: 3px;
120 |
121 | }
122 |
123 | .video-js .next-overlay .vjs-nav-prev .icon {
124 | -webkit-transform: rotate(135deg);
125 | transform: rotate(135deg);
126 | }
127 | .video-js .next-overlay .vjs-nav-next .icon {
128 | -webkit-transform: rotate(-45deg);
129 | transform: rotate(-45deg);
130 | }
131 | .video-js .next-overlay .disabled {
132 | opacity:.35;
133 | cursor:none;
134 | }
135 | .video-js .vjs-fullnext .respo {
136 | width: 100%;
137 | position: relative;
138 | padding-top: 56.25%;
139 | height: auto;
140 | background:#000;
141 | }
142 | .video-js .vjs-fullnext .img {
143 | position: absolute;
144 | top: 0;
145 | left: 0;
146 | width:100%;
147 | height:100%;
148 | background-size: cover;
149 | background-repeat: no-repeat;
150 | background-position: center;
151 | }
152 |
153 | .vjs-480 .vjs-nextup {
154 | width: 65%;
155 | }
156 | .vjs-480 .vjs-nextup .next-header{
157 | font-size:18px;
158 | }
159 |
160 |
161 | .video-js .vjs-fullnext .full-dur {
162 | position: absolute;
163 | top: 0;
164 | left: 0;
165 | padding: 5px;
166 | background: rgba(0,0,0,.5);
167 | border-bottom-right-radius: 5px;
168 | color: #ccc;
169 | }
170 |
171 | .video-js .vjs-fullnext .next-title {
172 | padding: 0 10px 10px 10px;
173 | color:#fff;
174 | font-size:16px;
175 | background:linear-gradient(0deg, rgba(0, 0, 0, .6),transparent);
176 | position:absolute;
177 | bottom:0;
178 | left:0;
179 | width:100%;
180 | text-shadow: 1px 1px 1px #000;
181 | }
182 | .vjs-480 .vjs-fullnext .next-title {
183 | font-size:14px;
184 | }
185 |
186 | .video-js .vjs-fullnext img {
187 | width:100%;
188 | display:block;
189 | }
190 |
191 |
192 | .video-js .vjs-upnext {
193 | position: absolute;
194 | z-index: 12;
195 | background: #000;
196 | right: -100%;
197 | bottom: 0;
198 | margin: 0;
199 | padding: 0;
200 | height: 60px;
201 | overflow: hidden;
202 | font-weight:normal;
203 | font-family:Arial,sans-serif;
204 | font-size: 14px;
205 | cursor:pointer;
206 | border:solid 1px #444;
207 | -webkit-transition: all 0.5s ease-in-out;
208 | transition: all 0.5s ease-in-out;
209 | }
210 | .vjs-user-active .vjs-upnext {
211 | bottom: 65px;
212 | }
213 | .vjs-1600.vjs-user-active .vjs-upnext {
214 | bottom: 100px;
215 | }
216 | .vjs-paused:not(.vjs-touch-inactive) .vjs-upnext{
217 | bottom: 65px;
218 | }
219 | .vjs-1600.vjs-paused:not(vjs-touch-inactive) .vjs-upnext{
220 | bottom: 100px;
221 | }
222 |
223 | .vjs-touchactive .vjs-upnext {
224 | bottom: 65px;
225 | }
226 | .vjs-1600.vjs-touchactive .vjs-upnext {
227 | bottom: 90px;
228 | }
229 |
230 | .video-js .vjs-upnext-show {
231 | right:0;
232 | }
233 | .video-js .vjs-upnext img {
234 | border:0;
235 | float:left;
236 | height:60px;
237 | margin: 0 10px 0 0;
238 | padding:0;
239 | }
240 | .video-js .vjs-upnext .next-dur {
241 | position: absolute;
242 | font-size: 12px;
243 | font-family:Arial,sans-serif;
244 | color:#fff;
245 | padding: 0 2px;
246 | background: rgba(0,0,0,.75);
247 | bottom: 0;
248 | color:#fff;
249 | }
250 | .video-js .vjs-upnext .upnext-right {
251 | display: inline-block;
252 | vertical-align: top;
253 | padding: 0 8px 0 0;
254 | max-width: 170px;
255 | margin:0;
256 | }
257 | .video-js .vjs-upnext .nextup {
258 | margin: 0;
259 | padding: 4px 0 5px 0;
260 | font-weight: bold;
261 | font-family:Arial,sans-serif;
262 | display:block;
263 | color:#fff;
264 | }
265 | .video-js .vjs-upnext span {
266 | font-size: 12px;
267 | color: #ccc;
268 | padding:0;
269 | font-family:Arial,sans-serif;
270 | font-weight:normal;
271 | }
272 | .video-js .vjs-upnext .vjs-up-title {
273 | display: -webkit-box;
274 | -webkit-line-clamp: 2;
275 | -webkit-box-orient: vertical;
276 | overflow: hidden;
277 | max-height:30px;
278 | }
279 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
29 |
68 |
69 |
70 | {#if !isAdminPage}
71 |
72 |
73 |
74 |
87 |
All current data is sample data for demonstration purposes
88 |
89 |
90 |
91 | {/if}
92 |
93 |
94 |
95 | {@render children()}
96 |
97 |
98 |
99 |
100 |
108 |
109 |
110 |
111 |
112 |
113 |
179 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
60 |
61 | {#if loading}
62 | Loading...
63 | {:else if error}
64 | Error: {error}
65 | {:else}
66 |
67 |
68 |
69 |
70 | Most Recent
71 |
72 |
73 | {#each recentAll as mediaItem (mediaItem.id)}
74 |
75 |
76 |
77 | {/each}
78 |
79 |
80 |
81 |
82 | {#if recentSeries.length > 0}
83 |
84 | Recent Series
85 |
86 |
87 | {#each recentSeries as mediaItem (mediaItem.id)}
88 |
89 |
90 |
91 | {/each}
92 |
93 |
94 |
95 | {/if}
96 |
97 | {#if recentMovies.length > 0}
98 |
99 | Recent Movies
100 |
101 |
102 | {#each recentMovies as mediaItem (mediaItem.id)}
103 |
104 |
105 |
106 | {/each}
107 |
108 |
109 |
110 | {/if}
111 |
112 |
113 |
114 | {/if}
115 |
116 |
174 |
--------------------------------------------------------------------------------
/src/routes/4k/+page.svelte:
--------------------------------------------------------------------------------
1 |
51 |
52 |
53 | {#if hero4kMedia.length > 0}
54 |
55 | {/if}
56 |
57 | {#if other4kMedia.length > 0}
58 | All 4K Content
59 | {#if loading}
60 | Loading...
61 | {:else if error}
62 | Error: {error}
63 | {:else}
64 |
65 |
66 | {#each other4kMedia as mediaItem (mediaItem.id)}
67 |
68 |
69 |
70 | {/each}
71 |
72 |
73 | {/if}
74 | {:else}
75 | 4K Content
76 | {#if loading}
77 | Loading...
78 | {:else if error}
79 | Error: {error}
80 | {:else}
81 | No 4K content found.
82 | {/if}
83 | {/if}
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/routes/admin/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import type { LayoutServerLoad } from "./$types"
2 | import { redirect } from '@sveltejs/kit';
3 |
4 | export const load: LayoutServerLoad = async (event) => {
5 | const session = await event.locals.auth()
6 | if (!session?.user) {
7 | redirect(303, `/login`)
8 | }
9 | return {
10 | session,
11 | }
12 | }
--------------------------------------------------------------------------------
/src/routes/admin/+layout.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
Admin Panel
35 |
36 |
37 |
38 |
39 |
40 | {@render children()}
41 |
42 |
43 |
44 |
45 |
53 |
54 |
--------------------------------------------------------------------------------
/src/routes/admin/+page.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
Admin Dashboard
39 |
40 | {#if loading}
41 |
Loading dashboard data...
42 | {:else if error}
43 |
Error: {error}
44 | {:else}
45 |
46 |
47 |
48 |
Total Media
49 |
{totalMedia}
50 |
51 |
52 |
53 |
54 |
Total Channels
55 |
{totalChannels}
56 |
57 |
58 |
59 |
60 |
Quick Actions
61 |
65 | {/if}
66 |
--------------------------------------------------------------------------------
/src/routes/admin/channels/+page.svelte:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 | {#if loading}
48 | Loading channels...
49 | {:else if error}
50 | Error: {error}
51 | {:else if channels.length === 0}
52 | No channels found. Add one!
53 | {:else}
54 |
55 |
56 |
57 |
58 | ID |
59 | Name |
60 | Title |
61 | Country |
62 | Actions |
63 |
64 |
65 |
66 | {#each channels as channel (channel.id)}
67 |
68 | {channel.id} |
69 | {channel.name} |
70 | {channel.title || 'N/A'} |
71 | {channel.country || 'N/A'} |
72 |
73 |
74 |
75 | |
76 |
77 | {/each}
78 |
79 |
80 |
81 | {/if}
--------------------------------------------------------------------------------
/src/routes/admin/channels/[id]/edit/+page.svelte:
--------------------------------------------------------------------------------
1 |
86 |
87 | {#if loading}
88 | Loading channel data...
89 | {:else if formError}
90 | {formError}
91 | {:else}
92 |
156 | {/if}
--------------------------------------------------------------------------------
/src/routes/admin/channels/add/+page.svelte:
--------------------------------------------------------------------------------
1 |
61 |
62 |
--------------------------------------------------------------------------------
/src/routes/admin/login.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
--------------------------------------------------------------------------------
/src/routes/admin/login/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
--------------------------------------------------------------------------------
/src/routes/admin/media/+page.svelte:
--------------------------------------------------------------------------------
1 |
69 |
70 |
71 |
72 |
73 | {#if loading}
74 | Loading media entries...
75 | {:else if error}
76 | Error: {error}
77 | {:else if mediaEntries.length === 0}
78 | No media entries found. Add one!
79 | {:else}
80 |
115 | {/if}
--------------------------------------------------------------------------------
/src/routes/api/channels/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { createDatabase } from '$lib/server/db';
3 | import { channels } from '$lib/server/schema';
4 | import { channelSchema } from '$lib/schemas/channel';
5 | import { eq } from 'drizzle-orm';
6 |
7 | export async function GET({ platform }: { platform: any }) {
8 | try {
9 | const db = createDatabase(platform);
10 | const allChannels = await db.select().from(channels).all();
11 | return json(allChannels);
12 | } catch (error: any) {
13 | console.error('Error fetching channels:', error);
14 | return json({ error: 'Failed to fetch channels' }, { status: 500 });
15 | }
16 | }
17 |
18 | export async function POST({ request, locals, platform }: { request: Request; locals: any; platform: any }) {
19 | const session = await locals.auth();
20 |
21 | if (!session?.user) {
22 | return new Response(null, { status: 401, statusText: "Unauthorized" });
23 | }
24 | try {
25 | const db = createDatabase(platform);
26 | const body = await request.json();
27 | const validatedData = channelSchema.parse(body);
28 |
29 | const newChannel = await db.insert(channels).values(validatedData).returning().get();
30 | return json(newChannel, { status: 201 });
31 | } catch (error: any) {
32 | console.error('Error creating channel:', error);
33 | if (error.name === 'ZodError') {
34 | return json({ error: 'Validation failed', details: error.errors }, { status: 400 });
35 | }
36 | return json({ error: 'Failed to create channel' }, { status: 500 });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/routes/api/channels/[id]/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { createDatabase } from '$lib/server/db';
3 | import { channels } from '$lib/server/schema';
4 | import { channelSchema } from '$lib/schemas/channel';
5 | import { eq } from 'drizzle-orm';
6 |
7 | export async function GET({ params, platform }: { params: any; platform: any }) {
8 | try {
9 | const db = createDatabase(platform);
10 | const { id } = params;
11 | const channel = await db.select().from(channels).where(eq(channels.id, id)).get();
12 |
13 | if (channel) {
14 | return json(channel);
15 | } else {
16 | return json({ error: 'Channel not found' }, { status: 404 });
17 | }
18 | } catch (error: any) {
19 | console.error(`Error fetching channel with ID ${params.id}:`, error);
20 | return json({ error: 'Failed to fetch channel' }, { status: 500 });
21 | }
22 | }
23 |
24 | export async function PUT({ params, request, platform }: { params: any; request: Request; platform: any }) {
25 | try {
26 | const db = createDatabase(platform);
27 | const { id } = params;
28 | const body = await request.json();
29 | const validatedData = channelSchema.parse(body);
30 |
31 | const updatedChannel = await db.update(channels)
32 | .set(validatedData)
33 | .where(eq(channels.id, id))
34 | .returning()
35 | .get();
36 |
37 | if (updatedChannel) {
38 | return json(updatedChannel);
39 | } else {
40 | return json({ error: 'Channel not found' }, { status: 404 });
41 | }
42 | } catch (error: any) {
43 | console.error(`Error updating channel with ID ${params.id}:`, error);
44 | if (error.name === 'ZodError') {
45 | return json({ error: 'Validation failed', details: error.errors }, { status: 400 });
46 | }
47 | return json({ error: 'Failed to update channel' }, { status: 500 });
48 | }
49 | }
50 |
51 | export async function DELETE({ params, platform }: { params: any; platform: any }) {
52 | try {
53 | const db = createDatabase(platform);
54 | const { id } = params;
55 | const deletedChannel = await db.delete(channels)
56 | .where(eq(channels.id, id))
57 | .returning()
58 | .get();
59 |
60 | if (deletedChannel) {
61 | return json({ message: 'Channel deleted successfully' });
62 | } else {
63 | return json({ error: 'Channel not found' }, { status: 404 });
64 | }
65 | } catch (error: any) {
66 | console.error(`Error deleting channel with ID ${params.id}:`, error);
67 | return json({ error: 'Failed to delete channel' }, { status: 500 });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/routes/api/episodes/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { createDatabase } from '$lib/server/db';
3 | import { episodes } from '$lib/server/schema';
4 |
5 | export async function POST({ request, locals, platform }: { request: Request; locals: any; platform: any }) {
6 | const session = await locals.auth();
7 | if (!session?.user) {
8 | return new Response(null, { status: 401, statusText: "Unauthorized" });
9 | }
10 |
11 | try {
12 | const db = createDatabase(platform);
13 | const data = await request.json();
14 |
15 | const newEpisode = await db.insert(episodes)
16 | .values({
17 | id: crypto.randomUUID(),
18 | seriesId: data.seriesId,
19 | seasonNumber: data.seasonNumber,
20 | episodeNumber: data.episodeNumber,
21 | title: data.title,
22 | description: data.description,
23 | originalVideoUrl: data.originalVideoUrl,
24 | localVideoUrl: data.localVideoUrl,
25 | releaseDate: data.releaseDate,
26 | audioLanguageFormat: data.audioLanguageFormat,
27 | subtitlesInfo: data.subtitlesInfo,
28 | })
29 | .returning()
30 | .get();
31 |
32 | return json(newEpisode, { status: 201 });
33 | } catch (error) {
34 | console.error('Error creating episode:', error);
35 | return json({ error: 'Failed to create episode' }, { status: 500 });
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/routes/api/episodes/[id]/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { createDatabase } from '$lib/server/db';
3 | import { episodes } from '$lib/server/schema';
4 | import { eq } from 'drizzle-orm';
5 |
6 | export async function PUT({ params, request, locals, platform }: { params: any; request: Request; locals: any; platform: any }) {
7 | const session = await locals.auth();
8 | if (!session?.user) {
9 | return new Response(null, { status: 401, statusText: "Unauthorized" });
10 | }
11 |
12 | try {
13 | const db = createDatabase(platform);
14 | const { id } = params;
15 | const data = await request.json();
16 |
17 | const updatedEpisode = await db.update(episodes)
18 | .set({
19 | seasonNumber: data.seasonNumber,
20 | episodeNumber: data.episodeNumber,
21 | title: data.title,
22 | description: data.description,
23 | originalVideoUrl: data.originalVideoUrl,
24 | localVideoUrl: data.localVideoUrl,
25 | releaseDate: data.releaseDate,
26 | audioLanguageFormat: data.audioLanguageFormat,
27 | subtitlesInfo: data.subtitlesInfo,
28 | })
29 | .where(eq(episodes.id, id))
30 | .returning()
31 | .get();
32 |
33 | if (updatedEpisode) {
34 | return json(updatedEpisode);
35 | } else {
36 | return json({ error: 'Episode not found' }, { status: 404 });
37 | }
38 | } catch (error) {
39 | console.error('Error updating episode:', error);
40 | return json({ error: 'Failed to update episode' }, { status: 500 });
41 | }
42 | }
43 |
44 | export async function DELETE({ params, locals, platform }: { params: any; locals: any; platform: any }) {
45 | const session = await locals.auth();
46 | if (!session?.user) {
47 | return new Response(null, { status: 401, statusText: "Unauthorized" });
48 | }
49 |
50 | try {
51 | const db = createDatabase(platform);
52 | const { id } = params;
53 | const deletedEpisode = await db.delete(episodes)
54 | .where(eq(episodes.id, id))
55 | .returning()
56 | .get();
57 |
58 | if (deletedEpisode) {
59 | return json({ message: 'Episode deleted successfully' });
60 | } else {
61 | return json({ error: 'Episode not found' }, { status: 404 });
62 | }
63 | } catch (error) {
64 | console.error('Error deleting episode:', error);
65 | return json({ error: 'Failed to delete episode' }, { status: 500 });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/routes/api/media/[id]/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { createDatabase } from '$lib/server/db';
3 | import { media, channels, moviesFiles, episodes } from '$lib/server/schema';
4 | import { eq } from 'drizzle-orm';
5 |
6 | export async function GET({ params, platform }: { params: any; platform: any }) {
7 | try {
8 | const db = createDatabase(platform);
9 | const { id } = params;
10 | const mediaItem = await db.query.media.findFirst({
11 | where: eq(media.id, id),
12 | with: {
13 | moviesFiles: true,
14 | episodes: true,
15 | channel: true, // Include channel details
16 | },
17 | });
18 |
19 | if (!mediaItem) {
20 | return json({ error: 'Media not found' }, { status: 404 });
21 | }
22 |
23 | return json(mediaItem);
24 | } catch (error: any) {
25 | console.error('Error fetching media item:', error);
26 | return json({ error: 'Failed to fetch media item' }, { status: 500 });
27 | }
28 | }
29 |
30 | export async function PUT({ params, request, platform }: { params: any; request: Request; platform: any }) {
31 | try {
32 | const db = createDatabase(platform);
33 | const { id } = params;
34 | const data = await request.json();
35 | const { type, channelId, videoFiles, episodes: seriesEpisodes, ...rest } = data;
36 |
37 | // Validate channelId if provided
38 | if (channelId) {
39 | const existingChannel = await db.select().from(channels).where(eq(channels.id, channelId)).get();
40 | if (!existingChannel) {
41 | return json({ error: 'Invalid channelId provided' }, { status: 400 });
42 | }
43 | }
44 |
45 | // Update media record (exclude videoFiles and episodes from rest)
46 | const updatedMedia = await db.update(media).set({ ...rest, channelId, type }).where(eq(media.id, id)).returning().get();
47 |
48 | if (type === 'movie') {
49 | if (videoFiles && videoFiles.length > 0) {
50 | await db.delete(moviesFiles).where(eq(moviesFiles.movieId, id));
51 | const filesToInsert = videoFiles.map((file: any) => ({
52 | id: crypto.randomUUID(), // Generate unique ID for each file
53 | videoUrl: file.videoUrl,
54 | quality: file.quality,
55 | format: file.format,
56 | audioLanguageFormat: file.audioLanguageFormat,
57 | subtitlesInfo: file.subtitlesInfo,
58 | movieId: id,
59 | }));
60 | await db.insert(moviesFiles).values(filesToInsert);
61 | }
62 | } else if (type === 'series') {
63 | if (seriesEpisodes && seriesEpisodes.length > 0) {
64 | await db.delete(episodes).where(eq(episodes.seriesId, id));
65 | const episodesToInsert = seriesEpisodes.map((episode: any) => ({
66 | id: crypto.randomUUID(), // Generate unique ID for each episode
67 | seasonNumber: episode.seasonNumber,
68 | episodeNumber: episode.episodeNumber,
69 | title: episode.title,
70 | description: episode.description,
71 | originalVideoUrl: episode.originalVideoUrl,
72 | localVideoUrl: episode.localVideoUrl,
73 | releaseDate: episode.releaseDate,
74 | audioLanguageFormat: episode.audioLanguageFormat,
75 | subtitlesInfo: episode.subtitlesInfo,
76 | seriesId: id,
77 | }));
78 | await db.insert(episodes).values(episodesToInsert);
79 | }
80 | }
81 |
82 | if (updatedMedia) {
83 | return json(updatedMedia);
84 | } else {
85 | return json({ error: 'Media not found' }, { status: 404 });
86 | }
87 | } catch (error: any) {
88 | console.error(`Error updating media item with ID ${params.id}:`, error);
89 | if (error.name === 'ZodError') {
90 | return json({ error: 'Validation failed', details: error.errors }, { status: 400 });
91 | }
92 | return json({ error: 'Failed to update media item' }, { status: 500 });
93 | }
94 | }
95 |
96 | export async function DELETE({ params, platform }: { params: any; platform: any }) {
97 | try {
98 | const db = createDatabase(platform);
99 | const { id } = params;
100 | const deletedMedia = await db.delete(media).where(eq(media.id, id)).returning().get();
101 |
102 | if (deletedMedia) {
103 | return json({ message: 'Media item deleted successfully' });
104 | } else {
105 | return json({ error: 'Media item not found' }, { status: 404 });
106 | }
107 | } catch (error: any) {
108 | console.error(`Error deleting media item with ID ${params.id}:`, error);
109 | return json({ error: 'Failed to delete media item' }, { status: 500 });
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/routes/api/movie-files/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { createDatabase } from '$lib/server/db';
3 | import { moviesFiles } from '$lib/server/schema';
4 |
5 | export async function POST({ request, locals, platform }: { request: Request; locals: any; platform: any }) {
6 | const session = await locals.auth();
7 | if (!session?.user) {
8 | return new Response(null, { status: 401, statusText: "Unauthorized" });
9 | }
10 |
11 | try {
12 | const db = createDatabase(platform);
13 | const data = await request.json();
14 |
15 | const newFile = await db.insert(moviesFiles)
16 | .values({
17 | id: crypto.randomUUID(),
18 | movieId: data.movieId,
19 | videoUrl: data.videoUrl,
20 | quality: data.quality,
21 | format: data.format,
22 | audioLanguageFormat: data.audioLanguageFormat,
23 | subtitlesInfo: data.subtitlesInfo,
24 | })
25 | .returning()
26 | .get();
27 |
28 | return json(newFile, { status: 201 });
29 | } catch (error) {
30 | console.error('Error creating movie file:', error);
31 | return json({ error: 'Failed to create movie file' }, { status: 500 });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/routes/api/movie-files/[id]/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { createDatabase } from '$lib/server/db';
3 | import { moviesFiles } from '$lib/server/schema';
4 | import { eq } from 'drizzle-orm';
5 |
6 | export async function PUT({ params, request, locals, platform }: { params: any; request: Request; locals: any; platform: any }) {
7 | const session = await locals.auth();
8 | if (!session?.user) {
9 | return new Response(null, { status: 401, statusText: "Unauthorized" });
10 | }
11 |
12 | try {
13 | const db = createDatabase(platform);
14 | const { id } = params;
15 | const data = await request.json();
16 |
17 | const updatedFile = await db.update(moviesFiles)
18 | .set({
19 | videoUrl: data.videoUrl,
20 | quality: data.quality,
21 | format: data.format,
22 | audioLanguageFormat: data.audioLanguageFormat,
23 | subtitlesInfo: data.subtitlesInfo,
24 | })
25 | .where(eq(moviesFiles.id, id))
26 | .returning()
27 | .get();
28 |
29 | if (updatedFile) {
30 | return json(updatedFile);
31 | } else {
32 | return json({ error: 'Movie file not found' }, { status: 404 });
33 | }
34 | } catch (error) {
35 | console.error('Error updating movie file:', error);
36 | return json({ error: 'Failed to update movie file' }, { status: 500 });
37 | }
38 | }
39 |
40 | export async function DELETE({ params, locals, platform }: { params: any; locals: any; platform: any }) {
41 | const session = await locals.auth();
42 | if (!session?.user) {
43 | return new Response(null, { status: 401, statusText: "Unauthorized" });
44 | }
45 |
46 | try {
47 | const db = createDatabase(platform);
48 | const { id } = params;
49 | const deletedFile = await db.delete(moviesFiles)
50 | .where(eq(moviesFiles.id, id))
51 | .returning()
52 | .get();
53 |
54 | if (deletedFile) {
55 | return json({ message: 'Movie file deleted successfully' });
56 | } else {
57 | return json({ error: 'Movie file not found' }, { status: 404 });
58 | }
59 | } catch (error) {
60 | console.error('Error deleting movie file:', error);
61 | return json({ error: 'Failed to delete movie file' }, { status: 500 });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/routes/login/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
--------------------------------------------------------------------------------
/src/routes/movies/+page.svelte:
--------------------------------------------------------------------------------
1 |
52 |
53 |
54 | {#if heroMovies.length > 0}
55 |
56 | {/if}
57 |
58 | {#if otherMovies.length > 0}
59 | All Movies
60 | {#if loading}
61 | Loading...
62 | {:else if error}
63 | Error: {error}
64 | {:else}
65 |
66 |
67 | {#each otherMovies as movie (movie.id)}
68 |
69 |
70 |
71 | {/each}
72 |
73 |
74 | {/if}
75 | {:else}
76 | Movies
77 | {#if loading}
78 | Loading...
79 | {:else if error}
80 | Error: {error}
81 | {:else}
82 | No movies found.
83 | {/if}
84 | {/if}
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/routes/player/[id]/+page.svelte:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mediathekcommunity/website/99a369dba3df140ea34be603259ca42c2954102b/src/routes/player/[id]/+page.svelte
--------------------------------------------------------------------------------
/src/routes/series/+page.svelte:
--------------------------------------------------------------------------------
1 |
52 |
53 |
54 | {#if heroSeries.length > 0}
55 |
56 | {/if}
57 |
58 | {#if otherSeries.length > 0}
59 | All Series
60 | {#if loading}
61 | Loading...
62 | {:else if error}
63 | Error: {error}
64 | {:else}
65 |
66 |
67 | {#each otherSeries as seriesItem (seriesItem.id)}
68 |
69 |
70 |
71 | {/each}
72 |
73 |
74 | {/if}
75 | {:else}
76 | Series
77 | {#if loading}
78 | Loading...
79 | {:else if error}
80 | Error: {error}
81 | {:else}
82 | No series found.
83 | {/if}
84 | {/if}
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/routes/youth/+page.svelte:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 | {#if heroYouth.length > 0}
56 |
57 | {/if}
58 |
59 | {#if otherYouth.length > 0}
60 | All Youth Content
61 | {#if loading}
62 | Loading...
63 | {:else if error}
64 | Error: {error}
65 | {:else}
66 |
67 |
68 | {#each otherYouth as mediaItem (mediaItem.id)}
69 |
70 |
71 |
72 | {/each}
73 |
74 |
75 | {/if}
76 | {:else}
77 | Youth Content
78 | {#if loading}
79 | Loading...
80 | {:else if error}
81 | Error: {error}
82 | {:else}
83 | No youth content found.
84 | {/if}
85 | {/if}
86 |
87 |
88 |
--------------------------------------------------------------------------------
/static/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-node';
2 |
3 | /** @type {import('@sveltejs/kit').Config} */
4 | const config = {
5 | kit: {
6 | adapter: adapter({
7 | // Enable precompression for better performance
8 | precompress: true
9 | }),
10 | // Optimize service worker caching
11 | serviceWorker: {
12 | register: false
13 | },
14 | // Add cache headers for static assets
15 | paths: {
16 | assets: '',
17 | base: ''
18 | }
19 | }
20 | };
21 |
22 | export default config;
23 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import tailwindcss from '@tailwindcss/vite';
2 | import { sveltekit } from '@sveltejs/kit/vite';
3 | import { defineConfig } from 'vite';
4 |
5 | export default defineConfig({
6 | plugins: [tailwindcss(), sveltekit()]
7 | });
8 |
--------------------------------------------------------------------------------