├── .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 | [![mediathekcommunity/website context](https://badge.forgithub.com/mediathekcommunity/website)](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 | {slide.title} 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 | {mediaItem.title} 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 | {mediaItem.title} 7 |
8 |
9 |
10 | {#if mediaItem.logo_url} 11 | 12 | Logo 13 | 14 | {/if} 15 | {#if mediaItem.uhd} 16 | 17 | UHD 18 | 19 | {/if} 20 | {#if mediaItem.type === 'movie'} 21 | 22 | 23 | 24 | 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 | 80 | 86 | 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 |