diff --git a/sites/dazn.com/__data__/content.json b/sites/dazn.com/__data__/content.json new file mode 100644 index 0000000000..3871aef3b5 --- /dev/null +++ b/sites/dazn.com/__data__/content.json @@ -0,0 +1,154 @@ +{ + "Id": "LinearChannels", + "Title": "Kanäle", + "Tiles": [ + { + "Id": "Linear:bj5o60qt6uoe1clfdsev239pr", + "AssetId": "bj5o60qt6uoe1clfdsev239pr", + "Title": "DAZN 1", + "LogoImage": { + "ImageMimeType": "png", + "ImageType": "image-logo", + "Id": "Logo_LTV_DAZN_1" + }, + "LinearSchedule": { + "Now": { + "Title": "Spanish Copa del Rey Soccer", + "EpisodeTitle": "Atlético Madrid - FC Barcelona", + "Genre": [ + { + "name": "Soccer", + "MappedValue": [1] + } + ], + "Description": "Aus dem Riyadh Air Metropolitano in Madrid, Spanien.", + "Start": "2026-07-19T15:00:00Z", + "End": "2026-07-19T17:00:00Z", + "Image": { + "ImageMimeType": "png", + "ImageType": "image-portrait", + "Id": "DAZN_1" + }, + "BackgroundImage": { + "ImageMimeType": "jpg", + "ImageType": "image-background", + "Id": "111745_2026-07-19_EP037460110300_108429740567_2026-02-13T20:01:23Z_GNLZZGG002QL9SB" + }, + "GradientBackgroundImage": { + "ImageMimeType": "jpg", + "ImageType": "image-background", + "Id": "111745_2026-07-19_EP037460110300_108429740567_2026-02-13T20:01:23Z_GNLZZGG002QL9SB" + }, + "IsLive": false, + "EventYear": "2026", + "ProgramType": "Sports event", + "TvRating": null + }, + "Next": { + "Title": "The Late Kick 2026", + "EpisodeTitle": "Folge 6", + "Genre": [ + { + "name": "Soccer", + "MappedValue": [1] + } + ], + "Description": "", + "Start": "2026-07-19T17:00:00Z", + "End": "2026-07-19T17:50:00Z", + "Image": { + "ImageMimeType": "png", + "ImageType": "image-portrait", + "Id": "DAZN_1" + }, + "BackgroundImage": { + "ImageMimeType": "jpg", + "ImageType": "image-background", + "Id": "111745_2026-07-19_SH064823960000_108834056900_2026-06-16T01:52:26Z_p33375908_st_h10_aa" + }, + "GradientBackgroundImage": { + "ImageMimeType": "jpg", + "ImageType": "image-background", + "Id": "111745_2026-07-19_SH064823960000_108834056900_2026-06-16T01:52:26Z_p33375908_st_h10_aa" + }, + "IsLive": false, + "EventYear": "2026", + "ProgramType": "Sports non-event", + "TvRating": null + }, + "Later": [ + { + "Title": "Best of DAZN - Highlights", + "EpisodeTitle": "", + "Genre": [ + { + "name": "Entertainment" + } + ], + "Description": "Bei Best of DAZN erwarten euch ausgewählte Highlights.", + "Start": "2026-07-19T17:50:00Z", + "End": "2026-07-19T18:10:00Z", + "Image": { + "ImageMimeType": "png", + "ImageType": "image-portrait", + "Id": "DAZN_1" + }, + "BackgroundImage": { + "ImageMimeType": "jpg", + "ImageType": "image-background", + "Id": "111745_2026-07-19_SH048946860000_101077135689_2022-04-22T10:18:32Z_p19111987_i_h8_aa" + }, + "GradientBackgroundImage": { + "ImageMimeType": "jpg", + "ImageType": "image-background", + "Id": "111745_2026-07-19_SH048946860000_101077135689_2022-04-22T10:18:32Z_p19111987_i_h8_aa" + }, + "IsLive": false, + "EventYear": "2020", + "ProgramType": "Sports non-event", + "TvRating": null + }, + { + "Title": "Bundesliga Soccer", + "EpisodeTitle": "31. Spieltag", + "Genre": [ + { + "name": "Soccer", + "MappedValue": [1] + } + ], + "Description": "Berichtet wird vom 31. Spieltag der Fußball-Bundesliga.", + "Start": "2026-07-20T06:00:00Z", + "End": "2026-07-20T07:05:00Z", + "Image": { + "ImageMimeType": "png", + "ImageType": "image-portrait", + "Id": "DAZN_1" + }, + "BackgroundImage": { + "ImageMimeType": "jpg", + "ImageType": "image-background", + "Id": "111745_2026-07-20_SH037261360000_109454514209_2025-08-28T09:24:36Z_p7956966_i_h8_aj" + }, + "IsLive": false, + "EventYear": "2026", + "ProgramType": "Sports non-event", + "TvRating": null + } + ] + } + }, + { + "Id": "Linear:9dmerrb96eue11mjxhgbnyjho", + "AssetId": "9dmerrb96eue11mjxhgbnyjho", + "Title": "Rally TV", + "LogoImage": { + "ImageMimeType": "png", + "ImageType": "image-logo", + "Id": "Logo_Rally_TV" + }, + "LinearSchedule": null + } + ], + "Type": "Linear" +} diff --git a/sites/dazn.com/dazn.com.channels.xml b/sites/dazn.com/dazn.com.channels.xml new file mode 100644 index 0000000000..b9bcb14a06 --- /dev/null +++ b/sites/dazn.com/dazn.com.channels.xml @@ -0,0 +1,124 @@ + + + Eurosport 1 + DAZN 2 + DAZN FAST+ + DAZN RISE + NHL FAST Channel + Rally TV + Eurosport 2 + DAZN 1 + MLB Network + Red Bull TV + Unbeaten + DFB.TV + NFL Network + SPORTDIGITAL FUSSBALL + Unbeaten + Red Bull TV + NHL FAST Channel + NFL Network: Live 24/7 Coverage + NFL Network: Live 24/7 Coverage + Red Bull TV: Extreme Sports Channel + NHL FAST Channel + Unbeaten: 24/7 Global Sports Channel + Rally TV + MLB Network + Unbeaten: 24/7 Global Sports Channel + Rally TV + Red Bull TV + NFL Network + Rally TV + Red Bull TV: Extreme Sports Channel + NHL FAST Channel + NFL Network: Live 24/7 Coverage + Unbeaten: 24/7 Global Sports Channel + Red Bull TV: Extreme Sports Channel + DAZN 1 USA + Fubo Sports + Unbeaten: 24/7 Global Sports Channel + Rally TV + NFL Network + Red Bull TV: Extreme Sports Channel + Unbeaten: 24/7 Global Sports Channel + Rally TV + DSPORTS 2 + DSPORTS + DSPORTS + + DSPORTS + DSPORTS + Red Bull TV + Eurosport 2 + Eurosport 1 + LALIGA TV HYPERMOTION + Rally TV + DAZN 2 + NFL Network + NHL FAST Channel + DAZN 1 + DAZN F1® + DAZN Mundial + Unbeaten + DSPORTS + DSPORTS + DSPORTS + Rally TV + NFL Network + Red Bull TV + Unbeaten + NHL FAST Channel + NFL Network + NHL FAST Channel + Red Bull TV + Unbeaten + Unbeaten + MLB Network + Rally TV + Red Bull TV + Radio TV Serie A + Inter TV + Eurosport 4 + Real Time + Eurosport 1 + DAZN 1 + DMAX + NFL Network + Unbeaten + NHL FAST Channel + Eurosport 5 + Eurosport 3 + Eurosport 6 + Discovery + NOVE + Eurosport 2 + Red Bull TV + Rally TV + Milan TV + NHL FAST Channel + Unbeaten + レッドブルTV(24時間チャンネル) + NFL Network + Rally TV + Red Bull TV: Extreme Sports Channel + NHL FAST Channel + NFL Network: Live 24/7 Coverage + Unbeaten: 24/7 Global Sports Channel + Red Bull TV + NFL Network + Rally TV + Unbeaten + NHL FAST Channel + DAZN 2 + DAZN 5 + DAZN 3 + Red Bull TV + DAZN 1 + NFL Network + DAZN 4 + Unbeaten + Red Bull TV + Unbeaten + NHL FAST Channel + Rally TV + NFL Network + diff --git a/sites/dazn.com/dazn.com.config.js b/sites/dazn.com/dazn.com.config.js new file mode 100644 index 0000000000..faa43f65f7 --- /dev/null +++ b/sites/dazn.com/dazn.com.config.js @@ -0,0 +1,219 @@ +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +const API_URL = 'https://rail-router.discovery.indazn.com/eu/v10/Rail' +const IMAGE_API_URL = 'https://image.discovery.indazn.com/eu/v3/linear-channel/none' +const DEFAULT_COUNTRY = 'de' +const SUPPORTED_COUNTRIES = + 'ad ae af ag ai al am ao aq ar at au aw ax az ba bb bd be bf bg bh bi bj bl bm bn bo bq br bs bt bv bw bz ca cc cd cf cg ch ci ck cl cm co cr cv cw cx cy cz de dj dk dm do dz ec ee eg eh er es et fi fj fk fm fo fr ga gb gd ge gf gh gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il in io iq is it jm jo jp ke kg kh ki km kn kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mf mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn ps pt pw py qa re ro rs rw sa sb sc sd se sg sh si sj sk sl sn so sr ss st sv sx sz tc td tf tg th tj tk tl tm tn to tr tt tv tw tz ua ug um us uy uz vc ve vg vi vn vu wf ws xk ye yt za zm zw'.split( + ' ' + ) +const COUNTRY_LANGUAGES = { + ar: 'es', + at: 'de', + be: 'fr', + bo: 'es', + br: 'pt', + ch: 'de', + cl: 'es', + co: 'es', + cr: 'es', + de: 'de', + do: 'es', + ec: 'es', + es: 'es', + fr: 'fr', + gt: 'es', + hn: 'es', + it: 'it', + jp: 'ja', + li: 'de', + lu: 'fr', + mc: 'fr', + mx: 'es', + ni: 'es', + nl: 'nl', + pa: 'es', + pe: 'es', + pt: 'pt', + py: 'es', + sv: 'es', + tw: 'zh', + uy: 'es', + ve: 'es' +} +const PREFERRED_COUNTRIES = { + de: 'de', + en: 'gb', + es: 'es', + fr: 'fr', + it: 'it', + ja: 'jp', + nl: 'nl', + pt: 'pt', + zh: 'tw' +} +const HEADERS = { + Accept: 'application/json, text/plain, */*', + Referer: 'https://www.dazn.com/' +} + +module.exports = { + site: 'dazn.com', + days: 3, + url({ channel }) { + const { country } = parseSiteId(channel.site_id) + + return createApiUrl(country, channel.lang || getLanguage(country)) + }, + request: { + headers: HEADERS, + cache: { + ttl: 5 * 60 * 1000 + } + }, + parser({ content, channel, date }) { + const data = parseData(content) + const tiles = Array.isArray(data?.Tiles) ? data.Tiles : [] + const { assetId } = parseSiteId(channel.site_id) + const tile = tiles.find(item => item.AssetId === assetId) + + if (!tile?.LinearSchedule) return [] + + const schedule = tile.LinearSchedule + const programs = [ + schedule.Now, + schedule.Next, + ...(Array.isArray(schedule.Later) ? schedule.Later : []) + ].filter(Boolean) + + return programs + .filter(item => item.Start && item.End && dayjs.utc(item.Start).isSame(date, 'day')) + .map(parseProgram) + }, + async channels({ country, lang } = {}) { + if (country) return loadChannels(country, lang) + + const channelsByAssetAndLanguage = new Map() + for (const supportedCountry of SUPPORTED_COUNTRIES) { + for (const channel of await loadChannels(supportedCountry)) { + const { assetId, country } = parseSiteId(channel.site_id) + const key = `${assetId}#${channel.lang}` + const current = channelsByAssetAndLanguage.get(key) + + if (!current || isPreferredCountry(country, channel.lang)) { + channelsByAssetAndLanguage.set(key, channel) + } + } + } + + return Array.from(channelsByAssetAndLanguage.values()) + } +} + +async function loadChannels(country, lang) { + country = country.toLowerCase() + if (!SUPPORTED_COUNTRIES.includes(country)) { + throw new Error(`Unsupported DAZN country: ${country}`) + } + + const language = lang || getLanguage(country) + const { data } = await axios.get(createApiUrl(country, language), { headers: HEADERS }) + const tiles = Array.isArray(data?.Tiles) ? data.Tiles : [] + + return tiles + .filter(tile => tile.AssetId && tile.Title) + .map(tile => ({ + lang: language, + site_id: createSiteId(country, tile.AssetId), + name: tile.Title, + logo: createImageUrl(tile.LogoImage, { + resizeAction: 'contain', + width: 68, + height: 56, + format: 'png' + }) + })) +} + +function createApiUrl(country, language) { + const params = new URLSearchParams({ + platform: 'web', + id: 'Livetvschedule', + country, + brand: 'dazn', + languageCode: language + }) + + return `${API_URL}?${params}` +} + +function createSiteId(country, assetId) { + return country === DEFAULT_COUNTRY ? assetId : `${country}#${assetId}` +} + +function parseSiteId(siteId) { + const separator = siteId.indexOf('#') + + if (separator === -1) return { country: DEFAULT_COUNTRY, assetId: siteId } + + return { + country: siteId.slice(0, separator), + assetId: siteId.slice(separator + 1) + } +} + +function getLanguage(country) { + return COUNTRY_LANGUAGES[country] || 'en' +} + +function isPreferredCountry(country, language) { + return country === PREFERRED_COUNTRIES[language] +} + +function parseData(content) { + try { + return typeof content === 'string' ? JSON.parse(content) : content + } catch { + return null + } +} + +function parseProgram(item) { + const program = { + title: item.Title, + start: item.Start, + stop: item.End + } + + if (item.EpisodeTitle) program.subTitle = item.EpisodeTitle + if (item.Description) program.description = item.Description + if (item.EventYear) program.date = item.EventYear + if (item.TvRating) program.rating = item.TvRating + + const categories = (Array.isArray(item.Genre) ? item.Genre : []) + .map(genre => genre?.name) + .filter(Boolean) + if (item.ProgramType) categories.push(item.ProgramType) + if (categories.length) program.categories = [...new Set(categories)] + + const image = item.BackgroundImage || item.GradientBackgroundImage || item.Image + const icon = createImageUrl(image) + if (icon) program.icon = icon + + return program +} + +function createImageUrl( + image, + { resizeAction = 'fill', width = 856, height = 481, format = 'webp' } = {} +) { + if (!image?.Id) return null + + return `${IMAGE_API_URL}/${encodeURIComponent( + image.Id + )}/${resizeAction}/center/center/none/80/${width}/${height}/${format}/image?brand=dazn` +} diff --git a/sites/dazn.com/dazn.com.test.js b/sites/dazn.com/dazn.com.test.js new file mode 100644 index 0000000000..a11c547588 --- /dev/null +++ b/sites/dazn.com/dazn.com.test.js @@ -0,0 +1,122 @@ +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const { channels, parser, url } = require('./dazn.com.config.js') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +jest.mock('axios') + +const date = dayjs.utc('2026-07-19', 'YYYY-MM-DD').startOf('day') +const channel = { site_id: 'bj5o60qt6uoe1clfdsev239pr' } +const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://rail-router.discovery.indazn.com/eu/v10/Rail?platform=web&id=Livetvschedule&country=de&brand=dazn&languageCode=de' + ) +}) + +it('can generate regional url', () => { + expect(url({ channel: { site_id: 'es#asset-id', lang: 'es' } })).toBe( + 'https://rail-router.discovery.indazn.com/eu/v10/Rail?platform=web&id=Livetvschedule&country=es&brand=dazn&languageCode=es' + ) +}) + +it('can parse response', () => { + const results = parser({ content, channel, date }) + + expect(results).toHaveLength(3) + expect(results[0]).toMatchObject({ + title: 'Spanish Copa del Rey Soccer', + subTitle: 'Atlético Madrid - FC Barcelona', + description: 'Aus dem Riyadh Air Metropolitano in Madrid, Spanien.', + categories: ['Soccer', 'Sports event'], + date: '2026', + start: '2026-07-19T15:00:00Z', + stop: '2026-07-19T17:00:00Z', + icon: 'https://image.discovery.indazn.com/eu/v3/linear-channel/none/111745_2026-07-19_EP037460110300_108429740567_2026-02-13T20%3A01%3A23Z_GNLZZGG002QL9SB/fill/center/center/none/80/856/481/webp/image?brand=dazn' + }) + expect(results[2]).toMatchObject({ + title: 'Best of DAZN - Highlights', + description: 'Bei Best of DAZN erwarten euch ausgewählte Highlights.', + categories: ['Entertainment', 'Sports non-event'], + start: '2026-07-19T17:50:00Z', + stop: '2026-07-19T18:10:00Z' + }) +}) + +it('can filter response by date', () => { + const results = parser({ content, channel, date: date.add(1, 'day') }) + + expect(results).toHaveLength(1) + expect(results[0]).toMatchObject({ + title: 'Bundesliga Soccer', + start: '2026-07-20T06:00:00Z', + stop: '2026-07-20T07:05:00Z' + }) +}) + +it('can handle empty guide', () => { + expect(parser({ content: '', channel, date })).toEqual([]) + expect(parser({ content, channel: { site_id: '9dmerrb96eue11mjxhgbnyjho' }, date })).toEqual([]) +}) + +it('can create channel list', async () => { + axios.get.mockResolvedValue({ data: JSON.parse(content) }) + + const results = await channels({ country: 'de' }) + + expect(results).toHaveLength(2) + expect(results[0]).toMatchObject({ + lang: 'de', + site_id: 'bj5o60qt6uoe1clfdsev239pr', + name: 'DAZN 1', + logo: 'https://image.discovery.indazn.com/eu/v3/linear-channel/none/Logo_LTV_DAZN_1/contain/center/center/none/80/68/56/png/image?brand=dazn' + }) + expect(results[0]).not.toHaveProperty('xmltv_id') +}) + +it('can create one deduplicated channel list', async () => { + axios.get.mockClear() + axios.get.mockResolvedValue({ data: JSON.parse(content) }) + + const results = await channels() + + expect(axios.get).toHaveBeenCalledTimes(235) + expect(results).toHaveLength(18) + expect(results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ lang: 'de', site_id: 'bj5o60qt6uoe1clfdsev239pr' }), + expect.objectContaining({ lang: 'es', site_id: 'es#bj5o60qt6uoe1clfdsev239pr' }), + expect.objectContaining({ lang: 'en', site_id: 'gb#bj5o60qt6uoe1clfdsev239pr' }) + ]) + ) + + const keys = results.map(result => { + const assetId = result.site_id.includes('#') ? result.site_id.split('#')[1] : result.site_id + + return `${assetId}#${result.lang}` + }) + expect(new Set(keys).size).toBe(results.length) +}) + +it('can create regional channel list', async () => { + axios.get.mockResolvedValue({ data: JSON.parse(content) }) + + const results = await channels({ country: 'es', lang: 'es' }) + + expect(axios.get).toHaveBeenCalledWith( + 'https://rail-router.discovery.indazn.com/eu/v10/Rail?platform=web&id=Livetvschedule&country=es&brand=dazn&languageCode=es', + expect.any(Object) + ) + expect(results[0]).toMatchObject({ + lang: 'es', + site_id: 'es#bj5o60qt6uoe1clfdsev239pr', + name: 'DAZN 1' + }) +}) diff --git a/sites/dazn.com/readme.md b/sites/dazn.com/readme.md new file mode 100644 index 0000000000..a5f3c620c8 --- /dev/null +++ b/sites/dazn.com/readme.md @@ -0,0 +1,21 @@ +# dazn.com + +https://www.dazn.com/de-DE/epg-fixture/ + +### Download the guide + +```sh +npm run grab --- --sites=dazn.com +``` + +### Update channel list + +```sh +npm run channels:parse --- --config=./sites/dazn.com/dazn.com.config.js --output=./sites/dazn.com/dazn.com.channels.xml +``` + +### Test + +```sh +npm test --- dazn.com +```