From 3cb503d92790f1568804c79503c69e75b3366165 Mon Sep 17 00:00:00 2001 From: Deven Perez Date: Mon, 25 Aug 2025 20:58:26 -0400 Subject: [PATCH 1/3] Find channel by fuzzy search --- package.json | 13 ++++---- pnpm-lock.yaml | 9 ++++++ src/app/iptv/channel/route.ts | 60 +++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/app/iptv/channel/route.ts diff --git a/package.json b/package.json index 0600db4..4df10ae 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,20 @@ "lint": "next lint" }, "dependencies": { + "fuse.js": "^7.1.0", + "next": "15.4.6", "react": "19.1.0", - "react-dom": "19.1.0", - "next": "15.4.6" + "react-dom": "19.1.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.4.6", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 897ec77..f9046fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + fuse.js: + specifier: ^7.1.0 + version: 7.1.0 next: specifier: 15.4.6 version: 15.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1026,6 +1029,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2890,6 +2897,8 @@ snapshots: functions-have-names@1.2.3: {} + fuse.js@7.1.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 diff --git a/src/app/iptv/channel/route.ts b/src/app/iptv/channel/route.ts new file mode 100644 index 0000000..7f325f9 --- /dev/null +++ b/src/app/iptv/channel/route.ts @@ -0,0 +1,60 @@ +import { NextRequest } from "next/server" +import Fuse from "fuse.js" + +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 +} + +export async function GET(request: NextRequest) { + + const searchParams = request.nextUrl.searchParams + const name = searchParams.get('name') + + if (!name) { + return new Response("Must be supplied with a name to search", { status: 400 }) + } + + const channelResp = await fetch("https://iptv-org.github.io/api/channels.json") + + const channelData: ChannelInfo[] = await channelResp.json() + + const limitedChannelData = channelData.filter(c => ["US", "CA", "UK"].includes(c.country)) + + // const expandedChannelData = channelData.map(c => ({ + // ...c, + // all_names: [c.name, ...c.alt_names] + // })) + + // const filteredChannelData = expandedChannelData.filter(c => c.all_names.includes(name)) + + const fuse = new Fuse(limitedChannelData, { + keys: [ + "name", + "alt_names", + "id" + ], + includeScore: true, + distance: 20 + }) + + const found = fuse.search(name, { limit: 10 }) + + return new Response(JSON.stringify(found), { + headers: { + "Content-Type": "application/json" + } + }) +} \ No newline at end of file -- 2.47.3 From 99c1b861c72c363e887690594bb0046af9c713c6 Mon Sep 17 00:00:00 2001 From: Deven Perez Date: Mon, 25 Aug 2025 22:36:28 -0400 Subject: [PATCH 2/3] Memoized fuses --- src/app/iptv/channel/curated/route.ts | 74 +++++++++++++++++++ src/app/iptv/channel/route.ts | 102 ++++++++++++++++++++------ 2 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 src/app/iptv/channel/curated/route.ts diff --git a/src/app/iptv/channel/curated/route.ts b/src/app/iptv/channel/curated/route.ts new file mode 100644 index 0000000..1d7a94f --- /dev/null +++ b/src/app/iptv/channel/curated/route.ts @@ -0,0 +1,74 @@ +import { debugLog } from "@/utils/debugLog" +import { NextRequest } from "next/server" + +export async function GET(request: NextRequest) { + 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 = {} + 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[0].item.id + nameToIDMap[channelName] = foundId + })) + + return new Response(JSON.stringify(nameToIDMap), { + headers: { + 'Content-Type': 'application/json', + } + }) + +} \ No newline at end of file diff --git a/src/app/iptv/channel/route.ts b/src/app/iptv/channel/route.ts index 7f325f9..ef14212 100644 --- a/src/app/iptv/channel/route.ts +++ b/src/app/iptv/channel/route.ts @@ -1,5 +1,6 @@ import { NextRequest } from "next/server" import Fuse from "fuse.js" +import { debugLog } from "@/utils/debugLog"; type ChannelInfo = { id: string, @@ -18,41 +19,98 @@ type ChannelInfo = { website: string | null } +const manualNameToIdMappings: Record = { + // "5 USA": "5USA.uk", + // "Oxygen True Crime": "TrueCrime.uk", + // "CBC CA": "CBAFTDT.ca", + // "CMT USA": "CMTMusic.us", + // "Sportsnet West": "SportsnetWorld.ca", + // "Sportsnet East": "Sportsnet.ca", + // "Fox Sports 1 USA": "FoxSports2LatinAmerica.us", + // "BET USA": "IBTVUSA.us", + // "FOXNY USA": "FoxBusinessNetwork.us", + // "CBSNY USA": "CBNEspanol.us", + // "ABCNY USA": "MBC1USA.us", + // "Nat Geo Wild USA": "NatGeoKidsLatinAmerica.us", + // "CTV 2 Canada": "GTNCanada.ca", + // "AMC USA": "AMCRush.us", + // "ESPN USA": "ESPN3LatinAmerica.us", + // "MTV USA": "IBTVUSA.us", + // "IFC TV USA": "IndTVUSA.us", + // "FOX USA": "FoxBusinessNetwork.us", + // "Sportsnet Ontario": "SportsnetOne.ca", + // "NBCNY USA": "MBC1USA.us", + // "Showtime SHOxBET USA": "ShowtimeShowcase.us", + // "ESPN2 USA": "ESPN3LatinAmerica.us", + // "MY9TV USA": "MMCTVUSA.us", + // "MAVTV USA": "MMCTVUSA.us", + // "CW USA": "WUSADT1.us", + // "Discovery Life Channel": "DiscoveryChannel.ca", + // "The Food Network": "TheWordNetwork.us", + // "WETV USA": "IBTVUSA.us", + // "ION USA": "HmongUSATV.us", + // "CTV Canada": "GTNCanada.ca", + +} + +let memoChannelData: ChannelInfo[] | null = null; +let memoFuses: Record> = {} + 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 }) } - const channelResp = await fetch("https://iptv-org.github.io/api/channels.json") + let fuse: Fuse | null = null; + const channelData = memoChannelData || (await (await fetch("https://iptv-org.github.io/api/channels.json")).json() as ChannelInfo[]) + memoChannelData = channelData - const channelData: ChannelInfo[] = await channelResp.json() + // 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 limitedChannelData = channelData.filter(c => ["US", "CA", "UK"].includes(c.country)) - // const expandedChannelData = channelData.map(c => ({ - // ...c, - // all_names: [c.name, ...c.alt_names] - // })) + const found: { item: ChannelInfo }[] = fuse.search(name, { limit: 1 }) + const foundChannel: ChannelInfo | null = found[0].item ?? null - // const filteredChannelData = expandedChannelData.filter(c => c.all_names.includes(name)) - - const fuse = new Fuse(limitedChannelData, { - keys: [ - "name", - "alt_names", - "id" - ], - includeScore: true, - distance: 20 - }) - - const found = fuse.search(name, { limit: 10 }) - - return new Response(JSON.stringify(found), { + return new Response(foundChannel ? JSON.stringify(foundChannel) : null, { headers: { "Content-Type": "application/json" } -- 2.47.3 From a1eb7c4d04d98b56a5aa53a00a4a0583fb3d9a11 Mon Sep 17 00:00:00 2001 From: Deven Perez Date: Tue, 26 Aug 2025 00:08:58 -0400 Subject: [PATCH 3/3] Manual channel curation --- src/app/health/route.ts | 2 +- src/app/iptv/channel/curated/route.ts | 38 ++++--- src/app/iptv/channel/route.ts | 39 +------ src/app/iptv/sdlive-playlist.m3u8/route.ts | 6 +- src/utils/constants.ts | 126 +++++++++++++++++++++ 5 files changed, 156 insertions(+), 55 deletions(-) create mode 100644 src/utils/constants.ts diff --git a/src/app/health/route.ts b/src/app/health/route.ts index 20fe452..bf911cf 100644 --- a/src/app/health/route.ts +++ b/src/app/health/route.ts @@ -1,4 +1,4 @@ -export async function GET(request: Request) { +export async function GET() { return new Response('OK', { status: 200, }) diff --git a/src/app/iptv/channel/curated/route.ts b/src/app/iptv/channel/curated/route.ts index 1d7a94f..9e04075 100644 --- a/src/app/iptv/channel/curated/route.ts +++ b/src/app/iptv/channel/curated/route.ts @@ -1,7 +1,7 @@ +import { manualNameToIdMappings } from "@/utils/constants" import { debugLog } from "@/utils/debugLog" -import { NextRequest } from "next/server" -export async function GET(request: NextRequest) { +export async function GET() { const response = await fetch(`https://sdlive-1-internal.d-ho.me/playlist.m3u8`) if (!response.ok) { @@ -46,24 +46,26 @@ export async function GET(request: NextRequest) { else return false }) - const url = new URL(request.url) - const baseUrl = `${url.protocol}//${url.host}` + // const url = new URL(request.url) + // const baseUrl = `${url.protocol}//${url.host}` - const nameToIDMap: Record = {} - 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 nameToIDMap: Record = {} + // 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[0].item.id - nameToIDMap[channelName] = foundId - })) + // 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: { diff --git a/src/app/iptv/channel/route.ts b/src/app/iptv/channel/route.ts index ef14212..3ed4631 100644 --- a/src/app/iptv/channel/route.ts +++ b/src/app/iptv/channel/route.ts @@ -1,6 +1,7 @@ import { NextRequest } from "next/server" import Fuse from "fuse.js" import { debugLog } from "@/utils/debugLog"; +import { manualNameToIdMappings } from "@/utils/constants"; type ChannelInfo = { id: string, @@ -19,42 +20,8 @@ type ChannelInfo = { website: string | null } -const manualNameToIdMappings: Record = { - // "5 USA": "5USA.uk", - // "Oxygen True Crime": "TrueCrime.uk", - // "CBC CA": "CBAFTDT.ca", - // "CMT USA": "CMTMusic.us", - // "Sportsnet West": "SportsnetWorld.ca", - // "Sportsnet East": "Sportsnet.ca", - // "Fox Sports 1 USA": "FoxSports2LatinAmerica.us", - // "BET USA": "IBTVUSA.us", - // "FOXNY USA": "FoxBusinessNetwork.us", - // "CBSNY USA": "CBNEspanol.us", - // "ABCNY USA": "MBC1USA.us", - // "Nat Geo Wild USA": "NatGeoKidsLatinAmerica.us", - // "CTV 2 Canada": "GTNCanada.ca", - // "AMC USA": "AMCRush.us", - // "ESPN USA": "ESPN3LatinAmerica.us", - // "MTV USA": "IBTVUSA.us", - // "IFC TV USA": "IndTVUSA.us", - // "FOX USA": "FoxBusinessNetwork.us", - // "Sportsnet Ontario": "SportsnetOne.ca", - // "NBCNY USA": "MBC1USA.us", - // "Showtime SHOxBET USA": "ShowtimeShowcase.us", - // "ESPN2 USA": "ESPN3LatinAmerica.us", - // "MY9TV USA": "MMCTVUSA.us", - // "MAVTV USA": "MMCTVUSA.us", - // "CW USA": "WUSADT1.us", - // "Discovery Life Channel": "DiscoveryChannel.ca", - // "The Food Network": "TheWordNetwork.us", - // "WETV USA": "IBTVUSA.us", - // "ION USA": "HmongUSATV.us", - // "CTV Canada": "GTNCanada.ca", - -} - let memoChannelData: ChannelInfo[] | null = null; -let memoFuses: Record> = {} +const memoFuses: Record> = {} export async function GET(request: NextRequest) { @@ -70,6 +37,8 @@ export async function GET(request: NextRequest) { 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] diff --git a/src/app/iptv/sdlive-playlist.m3u8/route.ts b/src/app/iptv/sdlive-playlist.m3u8/route.ts index 4389931..497e2a8 100644 --- a/src/app/iptv/sdlive-playlist.m3u8/route.ts +++ b/src/app/iptv/sdlive-playlist.m3u8/route.ts @@ -1,3 +1,4 @@ +import { manualNameToIdMappings } from "@/utils/constants" import { debugLog } from "@/utils/debugLog" import { NextRequest } from "next/server" @@ -50,7 +51,10 @@ export async function GET(request: NextRequest) { return new Response("Internal Server Error", { status: 500 }) } - const newInfo = `#EXTINF:-1 tvg-id="${sdliveChannelNum}.sdlive" ${afterExtInf}` + const channelName = info.split(",").at(-1) ?? "" + const tvgid = manualNameToIdMappings[channelName] + + const newInfo = `#EXTINF:-1 ${tvgid ? `tvg-id="${tvgid}" tvg-group="dhome-curated"` : `tvg-id="${sdliveChannelNum}.sdlive"`} sdlive-id="${sdliveChannelNum}.sdlive" ${afterExtInf}` newLines.push(newInfo, streamURL) } diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..0ff224b --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,126 @@ +export const manualNameToIdMappings: Record = { + "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": "RacerNetwork.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": "Sportsnet.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", + "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" +} \ No newline at end of file -- 2.47.3