Compare commits
15 Commits
5593bf028b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fb0342d03 | |||
| 5e50d6af3a | |||
| 943361f9e9 | |||
| a1eb7c4d04 | |||
| 99c1b861c7 | |||
| 3cb503d927 | |||
| e776c8247d | |||
| a57431198c | |||
| 665727562d | |||
| 89a829dac0 | |||
| a733118d35 | |||
| 99a8f3c8ac | |||
| 802992f551 | |||
| 0341d5958f | |||
| 03f8adf33d |
+66
@@ -0,0 +1,66 @@
|
||||
# syntax=docker.io/docker/dockerfile:1
|
||||
|
||||
FROM node:18-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
# ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn run build; \
|
||||
elif [ -f package-lock.json ]; then npm run build; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
# ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
|
||||
# server.js is created by next build from the standalone output
|
||||
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "api-internal",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"fuse.js": "^7.1.0",
|
||||
"next": "15.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
Generated
+3782
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export async function GET() {
|
||||
return new Response('OK', {
|
||||
status: 200,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { manualNameToIdMappings } from "@/utils/constants"
|
||||
import { debugLog } from "@/utils/debugLog"
|
||||
|
||||
export async function GET() {
|
||||
const response = await fetch(`https://sdlive-1-internal.d-ho.me/playlist.m3u8`)
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response('Failed to fetch playlist', { status: 500 })
|
||||
}
|
||||
|
||||
const playlist = await response.text()
|
||||
|
||||
const lines = playlist.split("\n")
|
||||
debugLog(lines)
|
||||
|
||||
const channels: string[] = []
|
||||
|
||||
for (let i = 1; i + 1 < lines.length; i += 2) {
|
||||
const info = lines[i]
|
||||
const streamURL = lines[i + 1]
|
||||
|
||||
debugLog(`Attempting to parse: info: ${info}. streamURL: ${streamURL}`)
|
||||
|
||||
// Basic validation to hope that assumptions are valid
|
||||
if (!info.startsWith("#EXTINF:-1")) {
|
||||
console.error(`iptv/sdlive-playlist.m3u8: One of the info lines did not start with #EXTINF:. Instead got: ${info}`)
|
||||
return new Response("Internal Server Error", { status: 500 })
|
||||
} else if (!streamURL.match(/\/stream\/[0-9]+\.m3u8$/)) {
|
||||
console.error(`iptv/sdlive-playlist.m3u8: One of the streamURLs did not match expected ending. Instead got: ${streamURL}`)
|
||||
return new Response("Internal Server Error", { status: 500 })
|
||||
}
|
||||
|
||||
const channelName = info.split(",").at(-1)
|
||||
|
||||
if (channelName) channels.push(channelName)
|
||||
}
|
||||
|
||||
const oneoffs = ["BBC America (BBCA)", "CBC CA", "CBS Sports Network (CBSSN)", "CTV 2 Canada", "CTV Canada", "Citytv ", "Discovery Channel", "Discovery Family", "Discovery Life Channel", "Disney", "E! Entertainment Television", "FX Movie Channel", "FYI", "Fox News", "Freeform", "Game Show Network", "Global CA", "HGTV", "MSNBC ", "NBC Sports", "NICK", "National Geographic", "Oprah Winfrey Network (OWN)", "Oxygen True Crime", "Paramount Network", "RDS CA", "RDS 2 CA", "TLC", "The Food Network", "The Hallmark Channel", "The Weather Channel", "VICE TV", "WWE Network"]
|
||||
|
||||
const filteredChannels = channels.filter(name => {
|
||||
if (name.includes("USA")) return true
|
||||
else if (name.includes("UK") && name.match(/ITV|BBC/)) return true
|
||||
else if (name.includes("TSN") && name.match(/[1-5]/g)) return true
|
||||
else if (name.includes("Sportsnet") && name.match(/360|East|One|Ontario|West|World/g)) return true
|
||||
else if (oneoffs.includes(name)) return true
|
||||
else return false
|
||||
})
|
||||
|
||||
// const url = new URL(request.url)
|
||||
// const baseUrl = `${url.protocol}//${url.host}`
|
||||
|
||||
// const nameToIDMap: Record<string, string> = {}
|
||||
// await Promise.all(filteredChannels.map(async channelName => {
|
||||
// const country = channelName.endsWith("USA")
|
||||
// ? "US" :
|
||||
// channelName.endsWith("CA") || channelName.endsWith("Canada")
|
||||
// ? "CA" :
|
||||
// channelName.endsWith("UK")
|
||||
// ? "UK" :
|
||||
// null
|
||||
|
||||
// const response = await fetch(`${baseUrl}/iptv/channel?name=${encodeURIComponent(channelName)}${country ? `&country=${country}` : ""}`)
|
||||
// const channelInfo = await response.json()
|
||||
// const foundId: string = channelInfo.id
|
||||
// nameToIDMap[channelName] = foundId
|
||||
// }))
|
||||
|
||||
const nameToIDMap = filteredChannels.map(channelName => `${channelName} => ${manualNameToIdMappings[channelName]}`)
|
||||
|
||||
return new Response(JSON.stringify(nameToIDMap), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { NextRequest } from "next/server"
|
||||
import Fuse from "fuse.js"
|
||||
import { debugLog } from "@/utils/debugLog";
|
||||
import { manualNameToIdMappings } from "@/utils/constants";
|
||||
|
||||
type ChannelInfo = {
|
||||
id: string,
|
||||
name: string,
|
||||
alt_names: string[],
|
||||
network: string | null,
|
||||
owners: string[],
|
||||
country: string,
|
||||
subdivision: string | null,
|
||||
city: string | null,
|
||||
categories: string[],
|
||||
is_nsfw: boolean,
|
||||
launched: string | null,
|
||||
closed: string | null,
|
||||
replaced_by: string | null,
|
||||
website: string | null
|
||||
}
|
||||
|
||||
let memoChannelData: ChannelInfo[] | null = null;
|
||||
const memoFuses: Record<string, Fuse<ChannelInfo>> = {}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const name = searchParams.get('name')
|
||||
const country = searchParams.get('country')
|
||||
|
||||
if (!name) {
|
||||
return new Response("Must be supplied with a name to search", { status: 400 })
|
||||
}
|
||||
|
||||
let fuse: Fuse<ChannelInfo> | null = null;
|
||||
const channelData = memoChannelData || (await (await fetch("https://iptv-org.github.io/api/channels.json")).json() as ChannelInfo[])
|
||||
memoChannelData = channelData
|
||||
|
||||
debugLog(name)
|
||||
|
||||
// Skip logic for manual mappings
|
||||
if (manualNameToIdMappings[name]) {
|
||||
const manualId = manualNameToIdMappings[name]
|
||||
const channel: ChannelInfo | undefined = channelData.find(data => data.id === manualId)
|
||||
|
||||
const channelInfo = channel ?? { id: manualId, name }
|
||||
|
||||
return new Response(JSON.stringify(channelInfo), {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Memoize fuses
|
||||
if (!memoFuses[country ?? ""]) {
|
||||
debugLog(`Creating memoFuse for ${country ?? "null"}`)
|
||||
|
||||
const limitedChannelData = country
|
||||
? channelData.filter(c => c.country === country.toUpperCase())
|
||||
: channelData.filter(c => ["US", "CA", "UK"].includes(c.country))
|
||||
|
||||
memoFuses[country ?? ""] = new Fuse(limitedChannelData, {
|
||||
keys: [
|
||||
"name",
|
||||
"alt_names",
|
||||
"id"
|
||||
],
|
||||
includeScore: true,
|
||||
distance: 20
|
||||
})
|
||||
fuse = memoFuses[country ?? ""]
|
||||
} else {
|
||||
fuse = memoFuses[country ?? ""]
|
||||
}
|
||||
|
||||
|
||||
const found: { item: ChannelInfo }[] = fuse.search(name, { limit: 1 })
|
||||
const foundChannel: ChannelInfo | null = found[0].item ?? null
|
||||
|
||||
return new Response(foundChannel ? JSON.stringify(foundChannel) : null, {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NextRequest } from "next/server"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const url = new URL(request.url)
|
||||
const baseUrl = `${url.protocol}//${url.host}`
|
||||
const response = await fetch(`${baseUrl}/iptv/sdlive-playlist.m3u8?id=1`)
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NextRequest } from "next/server"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const url = new URL(request.url)
|
||||
const baseUrl = `${url.protocol}//${url.host}`
|
||||
const response = await fetch(`${baseUrl}/iptv/sdlive-playlist.m3u8?id=2`)
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { manualNameToIdMappings } from "@/utils/constants"
|
||||
import { debugLog } from "@/utils/debugLog"
|
||||
import { NextRequest } from "next/server"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const id = searchParams.get('id')
|
||||
|
||||
if (!id || !["1", "2"].includes(id)) {
|
||||
return new Response("Must be supplied with an id 1 or 2", { status: 400 })
|
||||
}
|
||||
|
||||
const response = await fetch(`https://sdlive-${id}-internal.d-ho.me/playlist.m3u8`)
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response('Failed to fetch playlist', { status: 500 })
|
||||
}
|
||||
|
||||
const playlist = await response.text()
|
||||
|
||||
const lines = playlist.split("\n")
|
||||
debugLog(lines)
|
||||
|
||||
const newLines = [lines[0]]
|
||||
|
||||
for (let i = 1; i + 1 < lines.length; i += 2) {
|
||||
const info = lines[i]
|
||||
const streamURL = lines[i + 1]
|
||||
|
||||
debugLog(`Attempting to parse: info: ${info}. streamURL: ${streamURL}`)
|
||||
|
||||
// Basic validation to hope that assumptions are valid
|
||||
if (!info.startsWith("#EXTINF:-1")) {
|
||||
console.error(`iptv/sdlive-playlist.m3u8: One of the info lines did not start with #EXTINF:. Instead got: ${info}`)
|
||||
return new Response("Internal Server Error", { status: 500 })
|
||||
} else if (info.includes("tvg-id")) {
|
||||
console.warn("iptv/sdlive-playlist.m3u8: One of the channels already has a tvg-id. Channel skipped.")
|
||||
newLines.push(info, streamURL)
|
||||
continue
|
||||
} else if (!streamURL.match(/\/stream\/[0-9]+\.m3u8$/)) {
|
||||
console.error(`iptv/sdlive-playlist.m3u8: One of the streamURLs did not match expected ending. Instead got: ${streamURL}`)
|
||||
return new Response("Internal Server Error", { status: 500 })
|
||||
}
|
||||
|
||||
// Add tvg-id. This will need to be better parsed
|
||||
const afterExtInf = info.slice(11)
|
||||
const sdliveChannelNum = streamURL.split("/").at(-1)?.slice(0, -5)
|
||||
|
||||
if (sdliveChannelNum == null) {
|
||||
console.error("iptv/sdlive-playlist.m3u8: One of the streamURLs was not a number")
|
||||
return new Response("Internal Server Error", { status: 500 })
|
||||
}
|
||||
|
||||
const channelName = info.split(",").at(-1) ?? ""
|
||||
const tvgid = manualNameToIdMappings[channelName]
|
||||
|
||||
const newInfo = `#EXTINF:-1 ${tvgid ? `tvg-id="${tvgid}" group-title="dhome-curated"` : `tvg-id="${sdliveChannelNum}.sdlive"`} sdlive-id="${sdliveChannelNum}.sdlive" ${afterExtInf}`
|
||||
|
||||
newLines.push(newInfo, streamURL)
|
||||
}
|
||||
|
||||
const parsedPlaylist = newLines.join("\n")
|
||||
|
||||
|
||||
return new Response(parsedPlaylist, {
|
||||
headers: {
|
||||
'Content-Disposition': 'attachment; filename="sdlive-playlist.m3u8"',
|
||||
'Content-Type': 'application/vnd.apple.mpegurl',
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
export const manualNameToIdMappings: Record<string, string> = {
|
||||
"5 USA": "5USA.uk",
|
||||
"A&E USA": "AE.us",
|
||||
"ABC USA": "ABC.us",
|
||||
"ABCNY USA": "ABC.us",
|
||||
"ACC Network USA": "ACCNetwork.us",
|
||||
"AMC USA": "AMC.us",
|
||||
"AXS TV USA": "AXSTV.us",
|
||||
"BBC America (BBCA)": "BBCAmerica.us",
|
||||
"BBC Four UK": "BBCFour.uk",
|
||||
"BBC One UK": "BBCOne.uk",
|
||||
"BBC Three UK": "BBCThree.uk",
|
||||
"BBC Two UK": "BBCTwo.uk",
|
||||
"BET USA": "BET.us",
|
||||
"BIG TEN Network (BTN USA)": "BigTenNetwork.us",
|
||||
"BeIN SPORTS USA": "beINSportsUSA.us",
|
||||
"Bravo USA": "Bravo.us",
|
||||
"CBC CA": "CBLTDT.ca",
|
||||
"CBS Sports Network (CBSSN)": "CBSSportsNetworkCanada.ca",
|
||||
"CBS USA": "CBS.us",
|
||||
"CBSNY USA": "CBS.us",
|
||||
"CMT USA": "CMT.us",
|
||||
"CNBC USA": "CNBC.us",
|
||||
"CNN USA": "CNN.us",
|
||||
"COZI TV USA": "CoziTV.us",
|
||||
"CTV 2 Canada": "CHCJDT.ca",
|
||||
"CTV Canada": "CFTODT.ca",
|
||||
"CW USA": "CW.us",
|
||||
"Cinemax USA": "Cinemax.us",
|
||||
"Comet USA": "Comet.us",
|
||||
"Cooking Channel USA": "CookingChannel.us",
|
||||
"Crime+ Investigation USA": "CrimePlusInvestigation.us",
|
||||
"Discovery Channel": "DiscoveryChannel.ca",
|
||||
"Discovery Family": "DiscoveryFamily.us",
|
||||
"Discovery Life Channel": "DiscoveryLife.us",
|
||||
"E! Entertainment Television": "E.us",
|
||||
"ESPN USA": "ESPN.us",
|
||||
"ESPN2 USA": "ESPN2.us",
|
||||
"ESPNU USA": "ESPNU.us",
|
||||
"FOX Deportes USA": "FoxDeportes.us",
|
||||
"FOX USA": "Fox.us",
|
||||
"FOXNY USA": "Fox.us",
|
||||
"FX Movie Channel": "FXMovieChannel.us",
|
||||
"FX USA": "FX.us",
|
||||
"FXX USA": "FXX.us",
|
||||
"FYI": "FYI.us",
|
||||
"Fox News": "FoxNewsChannel.us",
|
||||
"Fox Sports 1 USA": "FoxSports1.us",
|
||||
"Fox Sports 2 USA": "FoxSports2.us",
|
||||
"Freeform": "Freeform.us",
|
||||
"GOLF Channel USA": "GolfChannel.us",
|
||||
"Galavisi贸n USA": "Galavision.us",
|
||||
"Game Show Network": "GameShowNetwork.us",
|
||||
"Global CA": "CIIIDT.ca",
|
||||
"HBO Comedy USA": "HBOComedy.us",
|
||||
"HBO Family USA": "HBOFamily.us",
|
||||
"HBO Latino USA": "HBOLatino.us",
|
||||
"HBO Signature USA": "HBOSignature.us",
|
||||
"HBO USA": "HBO.us",
|
||||
"HBO Zone USA": "HBOZone.us",
|
||||
"HBO2 USA": "HBO2.us",
|
||||
"HGTV": "HGTV.ca",
|
||||
"History USA": "History.us",
|
||||
"IFC TV USA": "IFC.us",
|
||||
"ION USA": "IONTV.us",
|
||||
"ITV 1 UK": "ITV1.uk",
|
||||
"ITV 2 UK": "ITV2.uk",
|
||||
"ITV 3 UK": "ITV3.uk",
|
||||
"ITV 4 UK": "ITV4.uk",
|
||||
"Investigation Discovery (ID USA)": "InvestigationDiscovery.us",
|
||||
"Longhorn Network USA": "LonghornNetwork.us",
|
||||
"MASN USA": "MASN.us",
|
||||
"MAVTV USA": "MAVTV.us",
|
||||
"METV USA": "MercedEducationalTV.us",
|
||||
"MLB Network USA": "MLBNetwork.us",
|
||||
"MSG USA": "MSG.us",
|
||||
"MTV USA": "MTV.us",
|
||||
"MY9TV USA": "WWORDT1.us",
|
||||
"NBA TV USA": "NBATV.us",
|
||||
"NBC USA": "NBC.us",
|
||||
"NBCNY USA": "NBC.us",
|
||||
"NESN USA": "NESN.us",
|
||||
"NHL Network USA": "NHLNetwork.us",
|
||||
"NICK": "Nickelodeon.us",
|
||||
"Nat Geo Wild USA": "NationalGeographicWild.us",
|
||||
"NewsNation USA": "NewsNation.us",
|
||||
"Newsmax USA": "NewsmaxTV.us",
|
||||
"Oprah Winfrey Network (OWN)": "OWN.ca",
|
||||
"Oxygen True Crime": "Oxygen.us",
|
||||
"POP TV USA": "PopTV.us",
|
||||
"Paramount Network": "ParamountNetwork.us",
|
||||
"RDS 2 CA": "RDS2.ca",
|
||||
"RDS CA": "RDS.ca",
|
||||
"SEC Network USA": "SECNetwork.us",
|
||||
"SYFY USA": "Syfy.us",
|
||||
"Showtime SHOxBET USA": "ShoxBet.us",
|
||||
"Showtime USA": "Showtime.us",
|
||||
"Sportsnet 360": "Sportsnet360.ca",
|
||||
"Sportsnet East": "SportsnetEast.ca",
|
||||
"Sportsnet One": "SportsnetOne.ca",
|
||||
"Sportsnet Ontario": "SportsnetOntario.ca",
|
||||
"Sportsnet West": "SportsnetWest.ca",
|
||||
"Sportsnet World": "SportsnetWorld.ca",
|
||||
"TBS USA": "TBS.us",
|
||||
"TCM USA": "TCM.us",
|
||||
"TLC": "TLC.us",
|
||||
"TMC Channel USA": "TheMovieChannel.us",
|
||||
"TNT USA": "TNT.us",
|
||||
"TSN1": "TSN1.ca",
|
||||
"TSN2": "TSN2.ca",
|
||||
"TSN3": "TSN3.ca",
|
||||
"TSN4": "TSN4.ca",
|
||||
"TSN5": "TSN5.ca",
|
||||
"TUDN USA": "TUDN.us",
|
||||
"TV ONE USA": "TVOne.us",
|
||||
"The Food Network": "FoodNetwork.us",
|
||||
"The Hallmark Channel": "HallmarkChannel.us",
|
||||
"The Weather Channel": "TheWeatherChannel.us",
|
||||
"TruTV USA": "truTV.us",
|
||||
"USA Network": "USANetwork.us",
|
||||
"Universal Kids USA": "UniversalKids.us",
|
||||
"VH1 USA": "VH1.us",
|
||||
"VICE TV": "VICETV.us",
|
||||
"WETV USA": "WeTV.us",
|
||||
"WWE Network": "WWENetwork.ca",
|
||||
"YES Network USA": "YesNetwork.us"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const debugLog = (...data: any[]): void => {
|
||||
if (process.env.LOG_LEVEL === "DEBUG") {
|
||||
console.log(...data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user