From eb77245999b00917e897d0b533c4debf7e047d45 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Tue, 10 Oct 2023 05:13:42 +0300 Subject: [PATCH 01/22] Replace ts-node with tsx --- package-lock.json | 694 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 14 +- tsconfig.json | 4 - yarn.lock | 68 ++++- 4 files changed, 749 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index d012a6f3c..9ec111503 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "tough-cookie": "^4.1.3", "transliteration": "^2.2.0", "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", + "tsx": "^3.13.0", "unzipit": "^1.4.0", "wildcard-match": "^5.1.2" } @@ -608,6 +608,8 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -619,6 +621,8 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -634,6 +638,336 @@ "kuler": "^2.0.0" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1541,22 +1875,30 @@ "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "optional": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.2", @@ -2088,6 +2430,8 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "optional": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -3164,7 +3508,9 @@ "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "optional": true, + "peer": true }, "node_modules/cron": { "version": "2.4.3", @@ -3386,6 +3732,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "optional": true, + "peer": true, "engines": { "node": ">=0.3.1" } @@ -3621,6 +3969,42 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -4255,6 +4639,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -6842,6 +7237,14 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -7625,6 +8028,8 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "optional": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -7666,13 +8071,40 @@ "node_modules/ts-node/node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "optional": true, + "peer": true }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tsx": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.13.0.tgz", + "integrity": "sha512-rjmRpTu3as/5fjNq/kOkOtihgLxuIz6pbKdj9xwP4J5jOLkBxw/rjN5ANw+KyrrOXV5uB7HC8+SrrSJxT65y+A==", + "dependencies": { + "esbuild": "~0.18.20", + "get-tsconfig": "^4.7.2", + "source-map-support": "^0.5.21" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7813,7 +8245,9 @@ "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "optional": true, + "peer": true }, "node_modules/v8-to-istanbul": { "version": "9.1.0", @@ -8085,6 +8519,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -8501,6 +8937,8 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "optional": true, + "peer": true, "requires": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -8509,6 +8947,8 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "optional": true, + "peer": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -8526,6 +8966,138 @@ "kuler": "^2.0.0" } }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "optional": true + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -9222,22 +9794,30 @@ "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "optional": true, + "peer": true }, "@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "optional": true, + "peer": true }, "@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "optional": true, + "peer": true }, "@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "optional": true, + "peer": true }, "@types/babel__core": { "version": "7.20.2", @@ -9647,7 +10227,9 @@ "acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "optional": true, + "peer": true }, "agent-base": { "version": "6.0.2", @@ -10400,7 +10982,9 @@ "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "optional": true, + "peer": true }, "cron": { "version": "2.4.3", @@ -10559,7 +11143,9 @@ "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "optional": true, + "peer": true }, "diff-sequences": { "version": "29.6.3", @@ -10735,6 +11321,35 @@ "is-arrayish": "^0.2.1" } }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -11195,6 +11810,14 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" }, + "get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "requires": { + "resolve-pkg-maps": "^1.0.0" + } + }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -13072,6 +13695,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==" + }, "resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -13640,6 +14268,8 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "optional": true, + "peer": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -13659,7 +14289,9 @@ "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "optional": true, + "peer": true } } }, @@ -13668,6 +14300,28 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "tsx": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.13.0.tgz", + "integrity": "sha512-rjmRpTu3as/5fjNq/kOkOtihgLxuIz6pbKdj9xwP4J5jOLkBxw/rjN5ANw+KyrrOXV5uB7HC8+SrrSJxT65y+A==", + "requires": { + "esbuild": "~0.18.20", + "fsevents": "~2.3.3", + "get-tsconfig": "^4.7.2", + "source-map-support": "^0.5.21" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -13763,7 +14417,9 @@ "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "optional": true, + "peer": true }, "v8-to-istanbul": { "version": "9.1.0", @@ -13970,7 +14626,9 @@ "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "optional": true, + "peer": true }, "yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 8d1f97d47..f0be2987d 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "epg", "scripts": { - "api:load": "ts-node scripts/commands/api/load.ts", - "channels:lint": "ts-node scripts/commands/channels/lint.ts", - "channels:parse": "ts-node scripts/commands/channels/parse.ts", - "channels:editor": "ts-node scripts/commands/channels/editor.ts", - "channels:validate": "ts-node scripts/commands/channels/validate.ts", - "grab": "ts-node scripts/commands/epg/grab.ts", + "api:load": "npx tsx scripts/commands/api/load.ts", + "channels:lint": "npx tsx scripts/commands/channels/lint.ts", + "channels:parse": "npx tsx scripts/commands/channels/parse.ts", + "channels:editor": "npx tsx scripts/commands/channels/editor.ts", + "channels:validate": "npx tsx scripts/commands/channels/validate.ts", + "grab": "npx tsx scripts/commands/epg/grab.ts", "serve": "npx serve", "lint": "npx eslint ./scripts/**/*.ts ./sites/**/*.js ./tests/**/*.ts ./tests/**/*.js", "test": "TZ=Pacific/Nauru npx jest --runInBand", @@ -74,7 +74,7 @@ "tough-cookie": "^4.1.3", "transliteration": "^2.2.0", "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", + "tsx": "^3.13.0", "unzipit": "^1.4.0", "wildcard-match": "^5.1.2" } diff --git a/tsconfig.json b/tsconfig.json index 24e7b3d50..cb7b116a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,9 +11,5 @@ "./src/types" ], "allowJs": true - }, - "ts-node": { - "esm": true, - "transpileOnly": true } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 03faa80be..236b7f64d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -330,6 +330,11 @@ enabled "2.0.x" kuler "^2.0.0" +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" @@ -2188,6 +2193,34 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +esbuild@~0.18.20: + version "0.18.20" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz" + integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== + optionalDependencies: + "@esbuild/android-arm" "0.18.20" + "@esbuild/android-arm64" "0.18.20" + "@esbuild/android-x64" "0.18.20" + "@esbuild/darwin-arm64" "0.18.20" + "@esbuild/darwin-x64" "0.18.20" + "@esbuild/freebsd-arm64" "0.18.20" + "@esbuild/freebsd-x64" "0.18.20" + "@esbuild/linux-arm" "0.18.20" + "@esbuild/linux-arm64" "0.18.20" + "@esbuild/linux-ia32" "0.18.20" + "@esbuild/linux-loong64" "0.18.20" + "@esbuild/linux-mips64el" "0.18.20" + "@esbuild/linux-ppc64" "0.18.20" + "@esbuild/linux-riscv64" "0.18.20" + "@esbuild/linux-s390x" "0.18.20" + "@esbuild/linux-x64" "0.18.20" + "@esbuild/netbsd-x64" "0.18.20" + "@esbuild/openbsd-x64" "0.18.20" + "@esbuild/sunos-x64" "0.18.20" + "@esbuild/win32-arm64" "0.18.20" + "@esbuild/win32-ia32" "0.18.20" + "@esbuild/win32-x64" "0.18.20" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" @@ -2532,7 +2565,7 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.3.2: +fsevents@^2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -2589,6 +2622,13 @@ get-stream@^6.0.0: resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-tsconfig@^4.7.2: + version "4.7.2" + resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz" + integrity sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A== + dependencies: + resolve-pkg-maps "^1.0.0" + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -4265,6 +4305,11 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve.exports@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz" @@ -4460,6 +4505,14 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +source-map-support@^0.5.21: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" @@ -4730,7 +4783,7 @@ ts-jest@^29.1.1: semver "^7.5.3" yargs-parser "^21.0.1" -ts-node@^10.9.1, ts-node@>=9.0.0: +ts-node@>=9.0.0: version "10.9.1" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz" integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== @@ -4754,6 +4807,17 @@ tslib@^2.1.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tsx@^3.13.0: + version "3.13.0" + resolved "https://registry.npmjs.org/tsx/-/tsx-3.13.0.tgz" + integrity sha512-rjmRpTu3as/5fjNq/kOkOtihgLxuIz6pbKdj9xwP4J5jOLkBxw/rjN5ANw+KyrrOXV5uB7HC8+SrrSJxT65y+A== + dependencies: + esbuild "~0.18.20" + get-tsconfig "^4.7.2" + source-map-support "^0.5.21" + optionalDependencies: + fsevents "~2.3.3" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" From 870e8a11f3200f315009cc2d59b0b71d050230ed Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sat, 14 Oct 2023 17:10:10 +0300 Subject: [PATCH 02/22] Update @freearhey/core package --- package-lock.json | 14 +++++++------- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ec111503..33cc8890d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "license": "MIT", "dependencies": { "@alex_neo/jest-expect-message": "^1.0.5", - "@freearhey/core": "^0.2.1", + "@freearhey/core": "^0.3.0", "@octokit/core": "^4.1.0", "@types/cli-progress": "^3.11.3", "@types/fs-extra": "^11.0.2", @@ -1051,9 +1051,9 @@ } }, "node_modules/@freearhey/core": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.2.1.tgz", - "integrity": "sha512-kEdIxZClykKhGpgyCSlkwuVuSCCAWr3J5YvOUMJQDPgVAYvT5VbD8MYKPm+OwNi9T4HFmF6qqY90qwKJPoOXCA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.3.0.tgz", + "integrity": "sha512-JcXwBj8GFOPbXUuH50LiCizKlih3PSKqdTxO53QveCF73aFY80n76yUfMtGclF5hBlzxuOITjyx2ArcTc43nzg==", "dependencies": { "@types/fs-extra": "^11.0.2", "@types/lodash": "^4.14.198", @@ -9156,9 +9156,9 @@ } }, "@freearhey/core": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.2.1.tgz", - "integrity": "sha512-kEdIxZClykKhGpgyCSlkwuVuSCCAWr3J5YvOUMJQDPgVAYvT5VbD8MYKPm+OwNi9T4HFmF6qqY90qwKJPoOXCA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.3.0.tgz", + "integrity": "sha512-JcXwBj8GFOPbXUuH50LiCizKlih3PSKqdTxO53QveCF73aFY80n76yUfMtGclF5hBlzxuOITjyx2ArcTc43nzg==", "requires": { "@types/fs-extra": "^11.0.2", "@types/lodash": "^4.14.198", diff --git a/package.json b/package.json index f0be2987d..06c77273d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@alex_neo/jest-expect-message": "^1.0.5", - "@freearhey/core": "^0.2.1", + "@freearhey/core": "^0.3.0", "@octokit/core": "^4.1.0", "@types/cli-progress": "^3.11.3", "@types/fs-extra": "^11.0.2", diff --git a/yarn.lock b/yarn.lock index 236b7f64d..8efff141c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -362,10 +362,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@freearhey/core@^0.2.1": - version "0.2.1" - resolved "https://registry.npmjs.org/@freearhey/core/-/core-0.2.1.tgz" - integrity sha512-kEdIxZClykKhGpgyCSlkwuVuSCCAWr3J5YvOUMJQDPgVAYvT5VbD8MYKPm+OwNi9T4HFmF6qqY90qwKJPoOXCA== +"@freearhey/core@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@freearhey/core/-/core-0.3.0.tgz" + integrity sha512-JcXwBj8GFOPbXUuH50LiCizKlih3PSKqdTxO53QveCF73aFY80n76yUfMtGclF5hBlzxuOITjyx2ArcTc43nzg== dependencies: "@types/fs-extra" "^11.0.2" "@types/lodash" "^4.14.198" From 5acd2d94c1b99bd911a5ac29f77df3dd52416aae Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:02:25 +0300 Subject: [PATCH 03/22] Update grab.ts --- scripts/commands/epg/grab.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/commands/epg/grab.ts b/scripts/commands/epg/grab.ts index 97875867f..38102d99a 100644 --- a/scripts/commands/epg/grab.ts +++ b/scripts/commands/epg/grab.ts @@ -61,7 +61,9 @@ async function main() { let files: string[] = [] if (options.site) { - files = await storage.list(path.join(SITES_DIR, `${options.site}/*.channels.xml`)) + let pattern = path.join(SITES_DIR, options.site, '*.channels.xml') + pattern = pattern.replace(/\\/g, '/') + files = await storage.list(pattern) } else if (options.channels) { files = await storage.list(options.channels) } From 9ce8d2cd6c3b2e90ed9cce30e469f92b0b1c47ac Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:02:40 +0300 Subject: [PATCH 04/22] Update @freearhey/core package --- package-lock.json | 14 +++++++------- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33cc8890d..f889452ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "license": "MIT", "dependencies": { "@alex_neo/jest-expect-message": "^1.0.5", - "@freearhey/core": "^0.3.0", + "@freearhey/core": "^0.3.1", "@octokit/core": "^4.1.0", "@types/cli-progress": "^3.11.3", "@types/fs-extra": "^11.0.2", @@ -1051,9 +1051,9 @@ } }, "node_modules/@freearhey/core": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.3.0.tgz", - "integrity": "sha512-JcXwBj8GFOPbXUuH50LiCizKlih3PSKqdTxO53QveCF73aFY80n76yUfMtGclF5hBlzxuOITjyx2ArcTc43nzg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.3.1.tgz", + "integrity": "sha512-9n4p4DcRKWz4zU6XluDEGKxiFRwlr+Nhn6DJg5tCG6Cov/yvN/TfGACRGCmJKXkVvf4TSKxIE3hZSKEOMRZI2w==", "dependencies": { "@types/fs-extra": "^11.0.2", "@types/lodash": "^4.14.198", @@ -9156,9 +9156,9 @@ } }, "@freearhey/core": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.3.0.tgz", - "integrity": "sha512-JcXwBj8GFOPbXUuH50LiCizKlih3PSKqdTxO53QveCF73aFY80n76yUfMtGclF5hBlzxuOITjyx2ArcTc43nzg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.3.1.tgz", + "integrity": "sha512-9n4p4DcRKWz4zU6XluDEGKxiFRwlr+Nhn6DJg5tCG6Cov/yvN/TfGACRGCmJKXkVvf4TSKxIE3hZSKEOMRZI2w==", "requires": { "@types/fs-extra": "^11.0.2", "@types/lodash": "^4.14.198", diff --git a/package.json b/package.json index 06c77273d..24758d6a3 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@alex_neo/jest-expect-message": "^1.0.5", - "@freearhey/core": "^0.3.0", + "@freearhey/core": "^0.3.1", "@octokit/core": "^4.1.0", "@types/cli-progress": "^3.11.3", "@types/fs-extra": "^11.0.2", diff --git a/yarn.lock b/yarn.lock index 8efff141c..1c01ae6ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -362,10 +362,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@freearhey/core@^0.3.0": - version "0.3.0" - resolved "https://registry.npmjs.org/@freearhey/core/-/core-0.3.0.tgz" - integrity sha512-JcXwBj8GFOPbXUuH50LiCizKlih3PSKqdTxO53QveCF73aFY80n76yUfMtGclF5hBlzxuOITjyx2ArcTc43nzg== +"@freearhey/core@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@freearhey/core/-/core-0.3.1.tgz" + integrity sha512-9n4p4DcRKWz4zU6XluDEGKxiFRwlr+Nhn6DJg5tCG6Cov/yvN/TfGACRGCmJKXkVvf4TSKxIE3hZSKEOMRZI2w== dependencies: "@types/fs-extra" "^11.0.2" "@types/lodash" "^4.14.198" From cb52742a1b5146cd1d22f16b1189d366ae7c8399 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:18:56 +0300 Subject: [PATCH 05/22] Update configLoader.ts --- scripts/core/configLoader.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/core/configLoader.ts b/scripts/core/configLoader.ts index 0fd429ad5..2cff65d8f 100644 --- a/scripts/core/configLoader.ts +++ b/scripts/core/configLoader.ts @@ -1,9 +1,11 @@ import { SiteConfig } from 'epg-grabber' import _ from 'lodash' +import { pathToFileURL } from 'url' export class ConfigLoader { async load(filepath: string): Promise { - const config = (await import(filepath)).default + const fileUrl = pathToFileURL(filepath).toString() + const config = (await import(fileUrl)).default return _.merge( { From c49f6caabf6db589608a52d778f5822b9608056e Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:25:24 +0300 Subject: [PATCH 06/22] Install run-script-os package --- package-lock.json | 19 +++++++++++++++++++ package.json | 7 ++++++- yarn.lock | 5 +++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f889452ee..222ed686b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,9 @@ "tsx": "^3.13.0", "unzipit": "^1.4.0", "wildcard-match": "^5.1.2" + }, + "devDependencies": { + "run-script-os": "^1.1.6" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -7326,6 +7329,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/run-script-os": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz", + "integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==", + "dev": true, + "bin": { + "run-os": "index.js", + "run-script-os": "index.js" + } + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -13748,6 +13761,12 @@ "queue-microtask": "^1.2.2" } }, + "run-script-os": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz", + "integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==", + "dev": true + }, "rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", diff --git a/package.json b/package.json index 24758d6a3..9f64fbbc1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "grab": "npx tsx scripts/commands/epg/grab.ts", "serve": "npx serve", "lint": "npx eslint ./scripts/**/*.ts ./sites/**/*.js ./tests/**/*.ts ./tests/**/*.js", - "test": "TZ=Pacific/Nauru npx jest --runInBand", + "test": "run-script-os", + "test:win32": "SET TZ=Pacific/Nauru && npx jest --runInBand", + "test:default": "TZ=Pacific/Nauru npx jest --runInBand", "postinstall": "npm run api:load" }, "private": true, @@ -77,5 +79,8 @@ "tsx": "^3.13.0", "unzipit": "^1.4.0", "wildcard-match": "^5.1.2" + }, + "devDependencies": { + "run-script-os": "^1.1.6" } } diff --git a/yarn.lock b/yarn.lock index 1c01ae6ce..62e54274c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4363,6 +4363,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +run-script-os@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz" + integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw== + rxjs@^7.2.0, rxjs@^7.5.5: version "7.8.1" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" From f7aa8aaab61cd66743564204671604bbab8da4a7 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:53:42 +0300 Subject: [PATCH 07/22] Update grab.test.ts --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f64fbbc1..ed6bed420 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "serve": "npx serve", "lint": "npx eslint ./scripts/**/*.ts ./sites/**/*.js ./tests/**/*.ts ./tests/**/*.js", "test": "run-script-os", - "test:win32": "SET TZ=Pacific/Nauru && npx jest --runInBand", + "test:win32": "SET \"TZ=Pacific/Nauru\" && npx jest --runInBand", "test:default": "TZ=Pacific/Nauru npx jest --runInBand", "postinstall": "npm run api:load" }, From 0c0cc2af931b41c2ab3e4e3d9b9cd52d29a6772c Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sat, 14 Oct 2023 19:01:58 +0300 Subject: [PATCH 08/22] Update grab.test.ts --- tests/commands/epg/grab.test.ts | 50 +++++++++++++++------------------ 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/tests/commands/epg/grab.test.ts b/tests/commands/epg/grab.test.ts index 70c029bc5..7e4b0747a 100644 --- a/tests/commands/epg/grab.test.ts +++ b/tests/commands/epg/grab.test.ts @@ -1,6 +1,14 @@ import { execSync } from 'child_process' import fs from 'fs-extra' import path from 'path' +import os from 'os' + +let ENV_VAR = + 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "SITES_DIR=tests/__data__/input/epg-grab/sites" && SET "CURR_DATE=2022-10-20" && SET "DATA_DIR=tests/__data__/input/temp/data" &&' +} beforeEach(() => { fs.emptyDirSync('tests/__data__/output') @@ -8,10 +16,8 @@ beforeEach(() => { describe('epg:grab', () => { it('can grab epg by site name', () => { - execSync( - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data npm run grab -- --site=example.com --output=tests/__data__/output/guide.xml', - { encoding: 'utf8' } - ) + const cmd = `${ENV_VAR} npm run grab -- --site=example.com --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) expect(content('tests/__data__/output/guide.xml')).toEqual( content('tests/__data__/expected/guide2.xml') @@ -19,10 +25,8 @@ describe('epg:grab', () => { }) it('can grab epg with multiple channels.xml files', () => { - execSync( - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml', - { encoding: 'utf8' } - ) + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) expect(content('tests/__data__/output/guide.xml')).toEqual( content('tests/__data__/expected/guide.xml') @@ -30,10 +34,8 @@ describe('epg:grab', () => { }) it('can grab epg with gzip option enabled', () => { - execSync( - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --gzip', - { encoding: 'utf8' } - ) + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --gzip` + execSync(cmd, { encoding: 'utf8' }) expect(content('tests/__data__/output/guide.xml')).toEqual( content('tests/__data__/expected/guide.xml') @@ -45,10 +47,8 @@ describe('epg:grab', () => { }) it('can grab epg with wildcard as output', () => { - execSync( - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml', - { encoding: 'utf8' } - ) + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml` + execSync(cmd, { encoding: 'utf8' }) expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( content('tests/__data__/expected/guides/en/example.com.xml') @@ -60,10 +60,8 @@ describe('epg:grab', () => { }) it('can grab epg then language filter enabled', () => { - execSync( - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr', - { encoding: 'utf8' } - ) + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr` + execSync(cmd, { encoding: 'utf8' }) expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( content('tests/__data__/expected/guides/fr/example.com.xml') @@ -71,10 +69,8 @@ describe('epg:grab', () => { }) it('can grab epg using custom channels list', () => { - execSync( - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml', - { encoding: 'utf8' } - ) + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) expect(content('tests/__data__/output/guide.xml')).toEqual( content('tests/__data__/expected/guide.xml') @@ -82,10 +78,8 @@ describe('epg:grab', () => { }) it('it will raise an error if the timeout is exceeded', () => { - const stdout = execSync( - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=0', - { encoding: 'utf8' } - ) + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=0` + const stdout = execSync(cmd, { encoding: 'utf8' }) expect(stdout).toContain('ERR: Connection timeout') }) From f3ba9fbcfeceb3f0f85fc522ddc5aaba408a849d Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 09:27:41 +0300 Subject: [PATCH 09/22] Update tests --- tests/commands/channels/editor.test.ts | 14 ++++++++------ tests/commands/channels/lint.test.ts | 10 +++------- tests/commands/channels/parse.test.ts | 14 ++++++-------- tests/commands/channels/validate.test.ts | 24 ++++++++++-------------- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/tests/commands/channels/editor.test.ts b/tests/commands/channels/editor.test.ts index 8499eb032..2db6ebd37 100644 --- a/tests/commands/channels/editor.test.ts +++ b/tests/commands/channels/editor.test.ts @@ -1,5 +1,6 @@ import fs from 'fs-extra' import { execSync } from 'child_process' +import os from 'os' beforeEach(() => { fs.emptyDirSync('tests/__data__/output') @@ -11,12 +12,13 @@ beforeEach(() => { describe('channels:editor', () => { it('shows list of options for a channel', () => { - const stdout = execSync( - 'DATA_DIR=tests/__data__/input/temp/data npm run channels:editor -- tests/__data__/output/channels.xml', - { - encoding: 'utf8' - } - ) + let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' + if (os.platform() === 'win32') { + ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' + } + + const cmd = `${ENV_VAR} npm run channels:editor -- tests/__data__/output/channels.xml` + const stdout = execSync(cmd, { encoding: 'utf8' }) expect(stdout).toContain('CNN International | CNNInternational.us [new]') expect(stdout).toContain('CNN International Europe | CNNInternationalEurope.us [api]') diff --git a/tests/commands/channels/lint.test.ts b/tests/commands/channels/lint.test.ts index 794cad23a..0aab10c2a 100644 --- a/tests/commands/channels/lint.test.ts +++ b/tests/commands/channels/lint.test.ts @@ -8,13 +8,9 @@ type ExecError = { describe('channels:lint', () => { it('will show a message if the file contains a syntax error', () => { try { - const stdout = execSync( - 'npm run channels:lint -- --channels=tests/__data__/input/channels-lint/channels-lint.channels.xml', - { - encoding: 'utf8' - } - ) - console.log(stdout) + const cmd = + 'npm run channels:lint -- --channels=tests/__data__/input/channels-lint/channels-lint.channels.xml' + execSync(cmd, { encoding: 'utf8' }) process.exit(1) } catch (error) { expect((error as ExecError).status).toBe(1) diff --git a/tests/commands/channels/parse.test.ts b/tests/commands/channels/parse.test.ts index 3c1aa4e71..83d91d7ae 100644 --- a/tests/commands/channels/parse.test.ts +++ b/tests/commands/channels/parse.test.ts @@ -12,10 +12,9 @@ beforeEach(() => { describe('channels:parse', () => { it('can parse channels', () => { - execSync( - 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml', - { encoding: 'utf8' } - ) + const cmd = + 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml' + execSync(cmd, { encoding: 'utf8' }) expect(content('tests/__data__/output/channels.xml')).toEqual( content('tests/__data__/expected/sites/channels-parse/channels-parse.channels.xml') @@ -23,10 +22,9 @@ describe('channels:parse', () => { }) it('can parse channels with clean flag', () => { - execSync( - 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml --clean', - { encoding: 'utf8' } - ) + const cmd = + 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml --clean' + execSync(cmd, { encoding: 'utf8' }) expect(content('tests/__data__/output/channels.xml')).toEqual( content('tests/__data__/expected/sites/channels-parse/channels-parse-clean.channels.xml') diff --git a/tests/commands/channels/validate.test.ts b/tests/commands/channels/validate.test.ts index 635e61649..d7a4946f2 100644 --- a/tests/commands/channels/validate.test.ts +++ b/tests/commands/channels/validate.test.ts @@ -1,20 +1,21 @@ import { execSync } from 'child_process' +import os from 'os' type ExecError = { status: number stdout: string } +let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' +if (os.platform() === 'win32') { + ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' +} + describe('channels:validate', () => { it('will show a message if the file contains a duplicate', () => { try { - const stdout = execSync( - 'DATA_DIR=tests/__data__/input/temp/data npm run channels:validate -- --channels=tests/__data__/input/channels-validate/duplicate.channels.xml', - { - encoding: 'utf8' - } - ) - console.log(stdout) + const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/duplicate.channels.xml` + execSync(cmd, { encoding: 'utf8' }) process.exit(1) } catch (error) { expect((error as ExecError).status).toBe(1) @@ -33,13 +34,8 @@ describe('channels:validate', () => { it('will show a message if the file contains a channel with wrong xmltv_id', () => { try { - const stdout = execSync( - 'DATA_DIR=tests/__data__/input/temp/data npm run channels:validate -- --channels=tests/__data__/input/channels-validate/wrong_xmltv_id.channels.xml', - { - encoding: 'utf8' - } - ) - console.log(stdout) + const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/wrong_xmltv_id.channels.xml` + execSync(cmd, { encoding: 'utf8' }) process.exit(1) } catch (error) { expect((error as ExecError).status).toBe(1) From 11856161c2399ac503175fd1f8d8661bed060f76 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 09:48:30 +0300 Subject: [PATCH 10/22] Create QueueCreator --- scripts/commands/epg/grab.ts | 6 +-- scripts/core/index.ts | 1 + scripts/core/queue.ts | 91 +++++++++--------------------------- scripts/core/queueCreator.ts | 71 ++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 73 deletions(-) create mode 100644 scripts/core/queueCreator.ts diff --git a/scripts/commands/epg/grab.ts b/scripts/commands/epg/grab.ts index 38102d99a..9741c8d7f 100644 --- a/scripts/commands/epg/grab.ts +++ b/scripts/commands/epg/grab.ts @@ -1,7 +1,7 @@ import { Logger, Timer, Storage, Collection } from '@freearhey/core' import { program } from 'commander' import { CronJob } from 'cron' -import { Queue, Job, ChannelsParser } from '../../core' +import { QueueCreator, Job, ChannelsParser } from '../../core' import { Channel } from 'epg-grabber' import path from 'path' import { SITES_DIR } from '../../constants' @@ -78,12 +78,12 @@ async function main() { logger.info(` found ${parsedChannels.count()} channels`) logger.info('creating queue...') - const queue = new Queue({ + const queueCreator = new QueueCreator({ parsedChannels, logger, options }) - await queue.create() + const queue = await queueCreator.create() logger.info(` added ${queue.size()} items`) const job = new Job({ diff --git a/scripts/core/index.ts b/scripts/core/index.ts index 6ab0d3408..0af8e6365 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -9,3 +9,4 @@ export * from './guideManager' export * from './guide' export * from './apiChannel' export * from './apiClient' +export * from './queueCreator' diff --git a/scripts/core/queue.ts b/scripts/core/queue.ts index c197da654..28af9d881 100644 --- a/scripts/core/queue.ts +++ b/scripts/core/queue.ts @@ -1,9 +1,5 @@ -import { Storage, Collection, DateTime, Logger, Dictionary } from '@freearhey/core' -import { ChannelsParser, ConfigLoader, ApiChannel } from './' -import { SITES_DIR, DATA_DIR, CURR_DATE } from '../constants' -import { Channel, SiteConfig } from 'epg-grabber' -import path from 'path' -import { GrabOptions } from '../commands/epg/grab' +import { Dictionary } from '@freearhey/core' +import { SiteConfig, Channel } from 'epg-grabber' export type QueueItem = { channel: Channel @@ -12,83 +8,38 @@ export type QueueItem = { error: string | null } -type QueueProps = { - logger: Logger - options: GrabOptions - parsedChannels: Collection -} - export class Queue { - configLoader: ConfigLoader - logger: Logger - sitesStorage: Storage - dataStorage: Storage - parser: ChannelsParser - parsedChannels: Collection - options: GrabOptions - date: DateTime - _items: QueueItem[] = [] + _data: Dictionary - constructor({ parsedChannels, logger, options }: QueueProps) { - this.parsedChannels = parsedChannels - this.logger = logger - this.sitesStorage = new Storage() - this.dataStorage = new Storage(DATA_DIR) - this.parser = new ChannelsParser({ storage: new Storage() }) - this.date = new DateTime(CURR_DATE) - this.options = options - this.configLoader = new ConfigLoader() + constructor() { + this._data = new Dictionary() } - async create() { - const channelsContent = await this.dataStorage.json('channels.json') - const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) + missing(key: string): boolean { + return this._data.missing(key) + } - const queue = new Dictionary() - - for (const channel of this.parsedChannels.all()) { - if (!channel.site || !channel.xmltv_id) continue - if (this.options.lang && channel.lang !== this.options.lang) continue - - const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) - const config: SiteConfig = await this.configLoader.load(configPath) - - const found: ApiChannel = channels.first( - (_channel: ApiChannel) => _channel.id === channel.xmltv_id - ) - if (found) { - channel.logo = found.logo - } - - const days = this.options.days || config.days || 1 - const dates = Array.from({ length: days }, (_, day) => this.date.add(day, 'd')) - dates.forEach((date: DateTime) => { - const dateString = date.toJSON() - const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}` - - if (queue.missing(key)) { - queue.set(key, { - channel, - date: dateString, - config, - error: null - }) - } - }) - } - - this._items = Object.values(queue.data()) + add( + key: string, + { channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig } + ) { + this._data.set(key, { + channel, + date, + config, + error: null + }) } size(): number { - return this._items.length + return Object.values(this._data.data()).length } items(): QueueItem[] { - return this._items + return Object.values(this._data.data()) as QueueItem[] } isEmpty(): boolean { - return this._items.length === 0 + return this.size() === 0 } } diff --git a/scripts/core/queueCreator.ts b/scripts/core/queueCreator.ts new file mode 100644 index 000000000..b23a8f4a8 --- /dev/null +++ b/scripts/core/queueCreator.ts @@ -0,0 +1,71 @@ +import { Storage, Collection, DateTime, Logger } from '@freearhey/core' +import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './' +import { SITES_DIR, DATA_DIR, CURR_DATE } from '../constants' +import { SiteConfig } from 'epg-grabber' +import path from 'path' +import { GrabOptions } from '../commands/epg/grab' + +type QueueCreatorProps = { + logger: Logger + options: GrabOptions + parsedChannels: Collection +} + +export class QueueCreator { + configLoader: ConfigLoader + logger: Logger + sitesStorage: Storage + dataStorage: Storage + parser: ChannelsParser + parsedChannels: Collection + options: GrabOptions + date: DateTime + + constructor({ parsedChannels, logger, options }: QueueCreatorProps) { + this.parsedChannels = parsedChannels + this.logger = logger + this.sitesStorage = new Storage() + this.dataStorage = new Storage(DATA_DIR) + this.parser = new ChannelsParser({ storage: new Storage() }) + this.date = new DateTime(CURR_DATE) + this.options = options + this.configLoader = new ConfigLoader() + } + + async create(): Promise { + const channelsContent = await this.dataStorage.json('channels.json') + const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) + + const queue = new Queue() + for (const channel of this.parsedChannels.all()) { + if (!channel.site || !channel.xmltv_id) continue + if (this.options.lang && channel.lang !== this.options.lang) continue + + const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) + const config: SiteConfig = await this.configLoader.load(configPath) + + const found: ApiChannel = channels.first( + (_channel: ApiChannel) => _channel.id === channel.xmltv_id + ) + if (found) { + channel.logo = found.logo + } + + const days = this.options.days || config.days || 1 + const dates = Array.from({ length: days }, (_, day) => this.date.add(day, 'd')) + dates.forEach((date: DateTime) => { + const dateString = date.toJSON() + const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}` + if (queue.missing(key)) { + queue.add(key, { + channel, + date: dateString, + config + }) + } + }) + } + + return queue + } +} From 6e2cc42bc68cd1c27dfe4b73b74204104afa2ab5 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 10:25:55 +0300 Subject: [PATCH 11/22] Fixes channels:editor script --- scripts/commands/channels/editor.ts | 3 +-- scripts/commands/channels/parse.ts | 7 ++++++- scripts/core/xml.ts | 6 ++---- .../sites/channels-editor/channels-editor.channels.xml | 4 ++++ tests/commands/channels/editor.test.ts | 10 ++++++++++ 5 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml diff --git a/scripts/commands/channels/editor.ts b/scripts/commands/channels/editor.ts index 0867ecf7c..dada24c9e 100644 --- a/scripts/commands/channels/editor.ts +++ b/scripts/commands/channels/editor.ts @@ -17,7 +17,6 @@ const programOptions = program.opts() const defaultCountry = programOptions.country.toLowerCase() const newLabel = ` [new]` -let site: string let options = new Collection() async function main() { @@ -119,7 +118,7 @@ function save() { .filter((option: { channel: Channel; delete: boolean }) => !option.delete) .map((option: { channel: Channel; delete: boolean }) => option.channel) - const xml = new XML(channels, site) + const xml = new XML(channels) storage.saveSync(filepath, xml.toString()) diff --git a/scripts/commands/channels/parse.ts b/scripts/commands/channels/parse.ts index 632236731..31f801251 100644 --- a/scripts/commands/channels/parse.ts +++ b/scripts/commands/channels/parse.ts @@ -50,6 +50,11 @@ async function main() { if (isPromise(parsedChannels)) { parsedChannels = await parsedChannels } + parsedChannels = parsedChannels.map((channel: Channel) => { + channel.site = config.site + + return channel + }) channels = channels .mergeBy( @@ -62,7 +67,7 @@ async function main() { (channel: Channel) => channel.site_id ]) - const xml = new XML(channels, config.site) + const xml = new XML(channels) await storage.save(outputFilepath, xml.toString()) diff --git a/scripts/core/xml.ts b/scripts/core/xml.ts index 1d7710b1f..8c644fa28 100644 --- a/scripts/core/xml.ts +++ b/scripts/core/xml.ts @@ -3,11 +3,9 @@ import { Channel } from 'epg-grabber' export class XML { items: Collection - site: string - constructor(items: Collection, site: string) { + constructor(items: Collection) { this.items = items - this.site = site } toString() { @@ -18,7 +16,7 @@ export class XML { const xmltv_id = channel.xmltv_id || '' const lang = channel.lang || '' const site_id = channel.site_id || '' - output += ` ${escapeString(channel.name)}\r\n` }) diff --git a/tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml b/tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml new file mode 100644 index 000000000..b06024fcb --- /dev/null +++ b/tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml @@ -0,0 +1,4 @@ + + + CNN International + diff --git a/tests/commands/channels/editor.test.ts b/tests/commands/channels/editor.test.ts index 2db6ebd37..4482475a3 100644 --- a/tests/commands/channels/editor.test.ts +++ b/tests/commands/channels/editor.test.ts @@ -1,6 +1,7 @@ import fs from 'fs-extra' import { execSync } from 'child_process' import os from 'os' +import path from 'path' beforeEach(() => { fs.emptyDirSync('tests/__data__/output') @@ -25,5 +26,14 @@ describe('channels:editor', () => { expect(stdout).toContain('Overwrite') expect(stdout).toContain('Skip') expect(stdout).toContain("File 'tests/__data__/output/channels.xml' successfully saved") + expect(content('tests/__data__/output/channels.xml')).toEqual( + content('tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml') + ) }) }) + +function content(filepath: string) { + return fs.readFileSync(path.resolve(filepath), { + encoding: 'utf8' + }) +} From 1757fa378cd71294d1bebaaa55f30e7e3cd98e1e Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 10:35:13 +0300 Subject: [PATCH 12/22] Update validate.test.ts --- tests/commands/channels/validate.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/commands/channels/validate.test.ts b/tests/commands/channels/validate.test.ts index d7a4946f2..3a305051c 100644 --- a/tests/commands/channels/validate.test.ts +++ b/tests/commands/channels/validate.test.ts @@ -19,16 +19,13 @@ describe('channels:validate', () => { process.exit(1) } catch (error) { expect((error as ExecError).status).toBe(1) - expect( - (error as ExecError).stdout - .includes(`tests/__data__/input/channels-validate/duplicate.channels.xml + expect((error as ExecError).stdout).toContain(`duplicate.channels.xml ┌─────────┬─────────────┬──────┬────────────────┬─────────┬─────────┐ │ (index) │ type │ lang │ xmltv_id │ site_id │ name │ ├─────────┼─────────────┼──────┼────────────────┼─────────┼─────────┤ │ 0 │ 'duplicate' │ 'en' │ 'BravoEast.us' │ '140' │ 'Bravo' │ └─────────┴─────────────┴──────┴────────────────┴─────────┴─────────┘ \n1 error(s) in 1 file(s)\n`) - ).toBe(true) } }) @@ -39,16 +36,13 @@ describe('channels:validate', () => { process.exit(1) } catch (error) { expect((error as ExecError).status).toBe(1) - expect( - (error as ExecError).stdout - .includes(`tests/__data__/input/channels-validate/wrong_xmltv_id.channels.xml + expect((error as ExecError).stdout).toContain(`wrong_xmltv_id.channels.xml ┌─────────┬──────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ │ (index) │ type │ lang │ xmltv_id │ site_id │ name │ ├─────────┼──────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ │ 0 │ 'wrong_xmltv_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ └─────────┴──────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ \n1 error(s) in 1 file(s)\n`) - ).toBe(true) } }) }) From e9ab5200404202eeba6e13ecef62bb458b09ef4a Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 10:43:02 +0300 Subject: [PATCH 13/22] Update lint.test.ts --- tests/commands/channels/lint.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/channels/lint.test.ts b/tests/commands/channels/lint.test.ts index 0aab10c2a..043c6e1b6 100644 --- a/tests/commands/channels/lint.test.ts +++ b/tests/commands/channels/lint.test.ts @@ -15,7 +15,7 @@ describe('channels:lint', () => { } catch (error) { expect((error as ExecError).status).toBe(1) expect((error as ExecError).stdout).toContain( - "tests/__data__/input/channels-lint/channels-lint.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n" + "channels-lint.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n" ) } }) From d2fa9a0e0f32e7c153771be57eac7055efb7b3f3 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 11:24:41 +0300 Subject: [PATCH 14/22] Update grab.test.ts --- tests/commands/epg/grab.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/commands/epg/grab.test.ts b/tests/commands/epg/grab.test.ts index 7e4b0747a..01e788f6f 100644 --- a/tests/commands/epg/grab.test.ts +++ b/tests/commands/epg/grab.test.ts @@ -41,8 +41,8 @@ describe('epg:grab', () => { content('tests/__data__/expected/guide.xml') ) - expect(content('tests/__data__/output/guide.xml.gz')).toEqual( - content('tests/__data__/expected/guide.xml.gz') + expect(fs.readFileSync('tests/__data__/output/guide.xml.gz')).toEqual( + fs.readFileSync('tests/__data__/expected/guide.xml.gz') ) }) From b3a9224a51ad7fb2c89c631034f4a23ee61bd968 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:10:31 +0300 Subject: [PATCH 15/22] Update grab.test.ts --- tests/commands/epg/grab.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/commands/epg/grab.test.ts b/tests/commands/epg/grab.test.ts index 01e788f6f..5d2b3b630 100644 --- a/tests/commands/epg/grab.test.ts +++ b/tests/commands/epg/grab.test.ts @@ -2,6 +2,7 @@ import { execSync } from 'child_process' import fs from 'fs-extra' import path from 'path' import os from 'os' +import { Zip } from '@freearhey/core' let ENV_VAR = 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data' @@ -33,7 +34,7 @@ describe('epg:grab', () => { ) }) - it('can grab epg with gzip option enabled', () => { + it('can grab epg with gzip option enabled', async () => { const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --gzip` execSync(cmd, { encoding: 'utf8' }) @@ -41,9 +42,10 @@ describe('epg:grab', () => { content('tests/__data__/expected/guide.xml') ) - expect(fs.readFileSync('tests/__data__/output/guide.xml.gz')).toEqual( - fs.readFileSync('tests/__data__/expected/guide.xml.gz') - ) + const zip = new Zip() + const expected = await zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) + const result = await zip.decompress(fs.readFileSync('tests/__data__/expected/guide.xml.gz')) + expect(expected).toEqual(result) }) it('can grab epg with wildcard as output', () => { From 74ce0a9a2e13fbb4ee780ba6c45ea0eb551fd84c Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:20:16 +0300 Subject: [PATCH 16/22] Update package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ed6bed420..09cc1fab8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "transform": { "^.+\\.(ts|js)$": "ts-jest" }, - "testRegex": "(tests|sites)/(.*?/)?.*test.(js|ts)$" + "testRegex": "(tests|sites)/(.*?/)?.*test.(js|ts)$", + "testTimeout": 10000 }, "dependencies": { "@alex_neo/jest-expect-message": "^1.0.5", From cd15eecd5f38b1a5a98b1464020b2b9ae48a7e62 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 13:44:04 +0300 Subject: [PATCH 17/22] Update editor.test.ts --- tests/commands/channels/editor.test.ts | 34 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/tests/commands/channels/editor.test.ts b/tests/commands/channels/editor.test.ts index 4482475a3..36c2f16cf 100644 --- a/tests/commands/channels/editor.test.ts +++ b/tests/commands/channels/editor.test.ts @@ -3,6 +3,11 @@ import { execSync } from 'child_process' import os from 'os' import path from 'path' +type ExecError = { + status: number + stdout: string +} + beforeEach(() => { fs.emptyDirSync('tests/__data__/output') fs.copySync( @@ -18,17 +23,24 @@ describe('channels:editor', () => { ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' } - const cmd = `${ENV_VAR} npm run channels:editor -- tests/__data__/output/channels.xml` - const stdout = execSync(cmd, { encoding: 'utf8' }) - - expect(stdout).toContain('CNN International | CNNInternational.us [new]') - expect(stdout).toContain('CNN International Europe | CNNInternationalEurope.us [api]') - expect(stdout).toContain('Overwrite') - expect(stdout).toContain('Skip') - expect(stdout).toContain("File 'tests/__data__/output/channels.xml' successfully saved") - expect(content('tests/__data__/output/channels.xml')).toEqual( - content('tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml') - ) + try { + const cmd = `${ENV_VAR} npm run channels:editor -- tests/__data__/output/channels.xml` + execSync(cmd, { encoding: 'utf8' }) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain('CNN International | CNNInternational.us [new]') + expect((error as ExecError).stdout).toContain( + 'CNN International Europe | CNNInternationalEurope.us [api]' + ) + expect((error as ExecError).stdout).toContain('Overwrite') + expect((error as ExecError).stdout).toContain('Skip') + expect((error as ExecError).stdout).toContain( + "File 'tests/__data__/output/channels.xml' successfully saved" + ) + expect(content('tests/__data__/output/channels.xml')).toEqual( + content('tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml') + ) + } }) }) From 1b4faee50dda6034b193e177ae95ea005dd2a9c0 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 13:51:46 +0300 Subject: [PATCH 18/22] Update .eslintrc.json --- .eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 1841e40db..c4f65cd0e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,7 +22,7 @@ "no-case-declarations": "off", "linebreak-style": [ "error", - "unix" + "windows" ], "quotes": [ "error", From 57e508fc3bf763e8de57e2c7822eade26da0b7ac Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 14:08:15 +0300 Subject: [PATCH 19/22] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09cc1fab8..46cbb961a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "channels:validate": "npx tsx scripts/commands/channels/validate.ts", "grab": "npx tsx scripts/commands/epg/grab.ts", "serve": "npx serve", - "lint": "npx eslint ./scripts/**/*.ts ./sites/**/*.js ./tests/**/*.ts ./tests/**/*.js", + "lint": "npx eslint \"{scripts,tests}/**/*.{ts,js}\"", "test": "run-script-os", "test:win32": "SET \"TZ=Pacific/Nauru\" && npx jest --runInBand", "test:default": "TZ=Pacific/Nauru npx jest --runInBand", From 63c86a2b30c793e69314be23f3e78680be913e7d Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Sun, 15 Oct 2023 14:08:23 +0300 Subject: [PATCH 20/22] Fixes linter errors --- scripts/commands/api/load.ts | 36 +- scripts/commands/channels/editor.ts | 360 ++++++------ scripts/commands/channels/lint.ts | 156 ++--- scripts/commands/channels/parse.ts | 166 +++--- scripts/commands/channels/validate.ts | 190 +++---- scripts/commands/epg/grab.ts | 234 ++++---- scripts/constants.ts | 8 +- scripts/core/apiChannel.ts | 158 +++--- scripts/core/apiClient.ts | 118 ++-- scripts/core/channelsParser.ts | 48 +- scripts/core/configLoader.ts | 42 +- scripts/core/date.js | 26 +- scripts/core/grabber.ts | 150 ++--- scripts/core/guide.ts | 110 ++-- scripts/core/guideManager.ts | 122 ++-- scripts/core/index.ts | 24 +- scripts/core/job.ts | 68 +-- scripts/core/queue.ts | 90 +-- scripts/core/queueCreator.ts | 142 ++--- scripts/core/xml.ts | 112 ++-- scripts/core/xmltv.ts | 56 +- scripts/types/langs.d.ts | 2 +- sites/9tv.co.il/9tv.co.il.config.js | 138 ++--- sites/9tv.co.il/9tv.co.il.test.js | 114 ++-- sites/abc.net.au/abc.net.au.config.js | 154 ++--- sites/abc.net.au/abc.net.au.test.js | 112 ++-- sites/allente.se/allente.se.config.js | 126 ++--- sites/allente.se/allente.se.test.js | 124 ++-- .../andorradifusio.ad.config.js | 118 ++-- .../andorradifusio.ad.test.js | 98 ++-- .../arianaafgtv.com/arianaafgtv.com.config.js | 164 +++--- .../arianatelevision.com.config.js | 120 ++-- .../arianatelevision.com.test.js | 122 ++-- sites/arirang.com/arirang.com.config.js | 306 +++++----- sites/arirang.com/arirang.com.test.js | 148 ++--- sites/artonline.tv/artonline.tv.config.js | 136 ++--- sites/artonline.tv/artonline.tv.test.js | 134 ++--- sites/astro.com.my/astro.com.my.config.js | 246 ++++---- sites/astro.com.my/astro.com.my.test.js | 146 ++--- sites/bein.com/bein.com.config.js | 158 +++--- sites/bein.com/bein.com.test.js | 120 ++-- sites/beinsports.com/beinsports.com.config.js | 260 ++++----- sites/beinsports.com/beinsports.com.test.js | 182 +++--- .../berrymedia.co.kr.config.js | 186 +++--- .../berrymedia.co.kr/berrymedia.co.kr.test.js | 158 +++--- sites/bt.com/bt.com.config.js | 108 ++-- sites/bt.com/bt.com.test.js | 104 ++-- sites/cablego.com.pe/cablego.com.pe.config.js | 218 +++---- sites/cablego.com.pe/cablego.com.pe.test.js | 108 ++-- .../cableplus.com.uy.config.js | 266 ++++----- .../cableplus.com.uy/cableplus.com.uy.test.js | 152 ++--- .../canalplus-caraibes.com.config.js | 186 +++--- .../canalplus-caraibes.com.test.js | 274 ++++----- .../canalplus-haiti.com.config.js | 188 +++---- .../canalplus-haiti.com.test.js | 352 ++++++------ .../canalplus-reunion.com.config.js | 144 ++--- .../canalplus-reunion.com.test.js | 320 +++++------ sites/canalplus.com/canalplus.com.config.js | 370 ++++++------ sites/canalplus.com/canalplus.com.test.js | 294 +++++----- sites/cgates.lt/cgates.lt.config.js | 184 +++--- sites/cgates.lt/cgates.lt.test.js | 104 ++-- .../chaines-tv.orange.fr.config.js | 94 ++-- .../chaines-tv.orange.fr.test.js | 96 ++-- .../clickthecity.com.config.js | 198 +++---- .../clickthecity.com/clickthecity.com.test.js | 140 ++--- sites/compulms.com/compulms.com.config.js | 66 +-- sites/compulms.com/compulms.com.test.js | 78 +-- sites/comteco.com.bo/comteco.com.bo.config.js | 136 ++--- sites/comteco.com.bo/comteco.com.bo.test.js | 148 ++--- sites/cosmote.gr/cosmote.gr.config.js | 156 ++--- sites/cosmote.gr/cosmote.gr.test.js | 158 +++--- sites/delta.nl/delta.nl.config.js | 140 ++--- sites/delta.nl/delta.nl.test.js | 140 ++--- .../digiturk.com.tr/digiturk.com.tr.config.js | 154 ++--- sites/digiturk.com.tr/digiturk.com.tr.test.js | 98 ++-- sites/directv.com.ar/directv.com.ar.config.js | 200 +++---- sites/directv.com.ar/directv.com.ar.test.js | 158 +++--- sites/directv.com.uy/directv.com.uy.config.js | 170 +++--- sites/directv.com.uy/directv.com.uy.test.js | 156 ++--- sites/directv.com/directv.com.config.js | 226 ++++---- sites/directv.com/directv.com.test.js | 196 +++---- sites/dishtv.in/dishtv.in.config.js | 290 +++++----- sites/dishtv.in/dishtv.in.test.js | 90 +-- sites/dsmart.com.tr/dsmart.com.tr.config.js | 208 +++---- sites/dsmart.com.tr/dsmart.com.tr.test.js | 136 ++--- sites/dstv.com/dstv.com.config.js | 202 +++---- sites/dstv.com/dstv.com.test.js | 224 ++++---- sites/elcinema.com/elcinema.com.config.js | 236 ++++---- sites/elcinema.com/elcinema.com.test.js | 138 ++--- .../ena.skylifetv.co.kr.config.js | 136 ++--- .../ena.skylifetv.co.kr.test.js | 118 ++-- .../entertainment.ie.config.js | 190 +++---- .../entertainment.ie/entertainment.ie.test.js | 118 ++-- .../epg.i-cable.com/epg.i-cable.com.config.js | 178 +++--- sites/epg.i-cable.com/epg.i-cable.com.test.js | 146 ++--- sites/firstmedia.com/firstmedia.com.config.js | 104 ++-- sites/firstmedia.com/firstmedia.com.test.js | 70 +-- sites/flixed.io/flixed.io.config.js | 90 +-- sites/flixed.io/flixed.io.test.js | 98 ++-- .../foxsports.com.au.config.js | 84 +-- .../foxsports.com.au/foxsports.com.au.test.js | 100 ++-- sites/foxtel.com.au/foxtel.com.au.config.js | 268 ++++----- sites/foxtel.com.au/foxtel.com.au.test.js | 124 ++-- sites/frikanalen.no/frikanalen.no.config.js | 104 ++-- sites/frikanalen.no/frikanalen.no.test.js | 98 ++-- sites/gatotv.com/gatotv.com.config.js | 200 +++---- sites/gatotv.com/gatotv.com.test.js | 164 +++--- .../getafteritmedia.com.config.js | 128 ++--- .../getafteritmedia.com.test.js | 94 ++-- sites/guidatv.sky.it/guidatv.sky.it.config.js | 144 ++--- sites/guidatv.sky.it/guidatv.sky.it.test.js | 104 ++-- sites/guide.dstv.com/guide.dstv.com.config.js | 188 +++---- sites/guide.dstv.com/guide.dstv.com.test.js | 118 ++-- sites/hd-plus.de/hd-plus.de.config.js | 114 ++-- sites/hd-plus.de/hd-plus.de.test.js | 112 ++-- sites/horizon.tv/horizon.tv.config.js | 290 +++++----- sites/horizon.tv/horizon.tv.test.js | 532 +++++++++--------- sites/i.mjh.nz/i.mjh.nz.config.js | 214 +++---- sites/i.mjh.nz/i.mjh.nz.test.js | 100 ++-- sites/i24news.tv/i24news.tv.config.js | 134 ++--- sites/i24news.tv/i24news.tv.test.js | 92 +-- sites/indihometv.com/indihometv.com.config.js | 136 ++--- sites/indihometv.com/indihometv.com.test.js | 116 ++-- sites/ionplustv.com/ionplustv.com.config.js | 214 +++---- sites/ionplustv.com/ionplustv.com.test.js | 100 ++-- sites/ipko.com/ipko.com.config.js | 92 +-- sites/ipko.com/ipko.com.test.js | 80 +-- sites/kan.org.il/kan.org.il.config.js | 104 ++-- sites/kan.org.il/kan.org.il.test.js | 96 ++-- sites/knr.gl/knr.gl.config.js | 130 ++--- sites/knr.gl/knr.gl.test.js | 104 ++-- sites/kplus.vn/kplus.vn.config.js | 158 +++--- sites/kplus.vn/kplus.vn.test.js | 134 ++--- sites/kvf.fo/kvf.fo.config.js | 142 ++--- sites/kvf.fo/kvf.fo.test.js | 88 +-- sites/m.tv.sms.cz/m.tv.sms.cz.config.js | 120 ++-- sites/m.tv.sms.cz/m.tv.sms.cz.test.js | 118 ++-- sites/magentatv.at/magentatv.at.config.js | 260 ++++----- sites/magentatv.at/magentatv.at.test.js | 194 +++---- sites/magentatv.de/magentatv.de.config.js | 246 ++++---- sites/magentatv.de/magentatv.de.test.js | 158 +++--- sites/magticom.ge/magticom.ge.config.js | 172 +++--- sites/magticom.ge/magticom.ge.test.js | 132 ++--- sites/mako.co.il/mako.co.il.config.js | 90 +-- sites/mako.co.il/mako.co.il.test.js | 84 +-- .../maxtv.hrvatskitelekom.hr.config.js | 90 +-- .../maxtv.hrvatskitelekom.hr.test.js | 110 ++-- sites/maxtvgo.mk/maxtvgo.mk.config.js | 144 ++--- sites/maxtvgo.mk/maxtvgo.mk.test.js | 146 ++--- sites/mbc.net/mbc.net.config.js | 80 +-- sites/mbc.net/mbc.net.test.js | 92 +-- .../mediagenie.co.kr.config.js | 154 ++--- .../mediagenie.co.kr/mediagenie.co.kr.test.js | 130 ++--- sites/mediaklikk.hu/mediaklikk.hu.config.js | 170 +++--- sites/mediaklikk.hu/mediaklikk.hu.test.js | 146 ++--- sites/mediaset.it/mediaset.it.config.js | 98 ++-- sites/melita.com/melita.com.config.js | 176 +++--- sites/melita.com/melita.com.test.js | 104 ++-- sites/meo.pt/meo.pt.config.js | 120 ++-- sites/meo.pt/meo.pt.test.js | 124 ++-- sites/mewatch.sg/mewatch.sg.config.js | 122 ++-- sites/mewatch.sg/mewatch.sg.test.js | 112 ++-- sites/mi.tv/mi.tv.config.js | 156 ++--- sites/mi.tv/mi.tv.test.js | 136 ++--- sites/mncvision.id/mncvision.id.config.js | 368 ++++++------ sites/mncvision.id/mncvision.id.test.js | 300 +++++----- sites/moji.id/moji.id.config.js | 244 ++++---- sites/moji.id/moji.id.test.js | 76 +-- .../mon-programme-tv.be.config.js | 200 +++---- .../mon-programme-tv.be.test.js | 132 ++--- .../movistarplus.es/movistarplus.es.config.js | 98 ++-- sites/movistarplus.es/movistarplus.es.test.js | 114 ++-- sites/mtel.ba/mtel.ba.config.js | 116 ++-- sites/mtel.ba/mtel.ba.test.js | 106 ++-- sites/mts.rs/mts.rs.config.js | 126 ++--- sites/mts.rs/mts.rs.test.js | 110 ++-- .../mujtvprogram.cz/mujtvprogram.cz.config.js | 138 ++--- sites/mujtvprogram.cz/mujtvprogram.cz.test.js | 112 ++-- sites/musor.tv/musor.tv.config.js | 174 +++--- sites/musor.tv/musor.tv.test.js | 120 ++-- .../myafn.dodmedia.osd.mil.config.js | 152 ++--- .../myafn.dodmedia.osd.mil.test.js | 116 ++-- sites/mysky.com.ph/mysky.com.ph.config.js | 124 ++-- sites/mysky.com.ph/mysky.com.ph.test.js | 94 ++-- sites/mytvsuper.com/mytvsuper.com.config.js | 164 +++--- sites/mytvsuper.com/mytvsuper.com.test.js | 142 ++--- sites/nhk.or.jp/nhk.or.jp.config.js | 140 ++--- sites/nhk.or.jp/nhk.or.jp.test.js | 90 +-- .../nhkworldpremium.com.config.js | 112 ++-- .../nhkworldpremium.com.test.js | 180 +++--- sites/nos.pt/nos.pt.config.js | 228 ++++---- sites/nos.pt/nos.pt.test.js | 200 +++---- sites/novacyprus.com/novacyprus.com.config.js | 134 ++--- sites/novacyprus.com/novacyprus.com.test.js | 100 ++-- sites/novasports.gr/novasports.gr.config.js | 174 +++--- sites/novasports.gr/novasports.gr.test.js | 98 ++-- .../nowplayer.now.com.config.js | 138 ++--- .../nowplayer.now.com.test.js | 120 ++-- .../nuevosiglo.com.uy.config.js | 212 +++---- .../nuevosiglo.com.uy.test.js | 200 +++---- .../ontvtonight.com/ontvtonight.com.config.js | 154 ++--- sites/ontvtonight.com/ontvtonight.com.test.js | 118 ++-- sites/osn.com/osn.com.config.js | 142 ++--- sites/osn.com/osn.com.test.js | 138 ++--- sites/pbsguam.org/pbsguam.org.config.js | 82 +-- sites/pbsguam.org/pbsguam.org.test.js | 96 ++-- sites/plex.tv/plex.tv.config.js | 180 +++--- sites/plex.tv/plex.tv.test.js | 118 ++-- .../programacion-tv.elpais.com.config.js | 210 +++---- .../programacion-tv.elpais.com.test.js | 148 ++--- .../programacion.tcc.com.uy.config.js | 204 +++---- .../programacion.tcc.com.uy.test.js | 158 +++--- sites/programetv.ro/programetv.ro.config.js | 160 +++--- sites/programetv.ro/programetv.ro.test.js | 140 ++--- .../programme-tv.net.config.js | 146 ++--- .../programme-tv.vini.pf.config.js | 176 +++--- .../programme-tv.vini.pf.test.js | 220 ++++---- .../programme.tvb.com.config.js | 128 ++--- .../programme.tvb.com.test.js | 94 ++-- .../programtv.onet.pl.config.js | 130 ++--- .../programtv.onet.pl.test.js | 154 ++--- sites/proximusmwc.be/proximusmwc.be.config.js | 160 +++--- sites/proximusmwc.be/proximusmwc.be.test.js | 138 ++--- sites/raiplay.it/raiplay.it.config.js | 158 +++--- sites/raiplay.it/raiplay.it.test.js | 102 ++-- sites/reportv.com.ar/reportv.com.ar.config.js | 338 +++++------ sites/reportv.com.ar/reportv.com.ar.test.js | 228 ++++---- sites/rev.bs/rev.bs.config.js | 136 ++--- sites/rev.bs/rev.bs.test.js | 166 +++--- sites/rotana.net/rotana.net.config.js | 132 ++--- sites/rotana.net/rotana.net.test.js | 114 ++-- sites/rtb.gov.bn/rtb.gov.bn.config.js | 148 ++--- sites/rtb.gov.bn/rtb.gov.bn.test.js | 192 +++---- sites/rthk.hk/rthk.hk.config.js | 178 +++--- sites/rthk.hk/rthk.hk.test.js | 164 +++--- .../rtmklik.rtm.gov.my.config.js | 90 +-- .../rtmklik.rtm.gov.my.test.js | 100 ++-- sites/rtp.pt/rtp.pt.config.js | 162 +++--- sites/rtp.pt/rtp.pt.test.js | 90 +-- sites/ruv.is/ruv.is.config.js | 158 +++--- sites/ruv.is/ruv.is.test.js | 96 ++-- sites/sat.tv/sat.tv.config.js | 258 ++++----- sites/sat.tv/sat.tv.test.js | 224 ++++---- sites/siba.com.co/siba.com.co.config.js | 112 ++-- sites/siba.com.co/siba.com.co.test.js | 108 ++-- sites/singtel.com/singtel.com.config.js | 94 ++-- sites/singtel.com/singtel.com.test.js | 124 ++-- sites/sjonvarp.is/sjonvarp.is.config.js | 128 ++--- sites/sjonvarp.is/sjonvarp.is.test.js | 100 ++-- sites/sky.co.nz/sky.co.nz.config.js | 110 ++-- sites/sky.co.nz/sky.co.nz.test.js | 124 ++-- sites/sky.com/sky.com.config.js | 66 +-- sites/sky.com/sky.com.test.js | 108 ++-- sites/sky.de/sky.de.config.js | 100 ++-- sites/sky.de/sky.de.test.js | 136 ++--- sites/sportsnet.ca/sportsnet.ca.config.js | 100 ++-- sites/sportsnet.ca/sportsnet.ca.test.js | 98 ++-- .../starhubtvplus.com.config.js | 170 +++--- .../starhubtvplus.com.test.js | 126 ++--- .../startimestv.com/startimestv.com.config.js | 230 ++++---- sites/startimestv.com/startimestv.com.test.js | 100 ++-- sites/startv.com/startv.com.config.js | 176 +++--- sites/startv.com/startv.com.test.js | 132 ++--- .../streamingtvguides.com.config.js | 140 ++--- .../streamingtvguides.com.test.js | 110 ++-- .../superguidatv.it/superguidatv.it.config.js | 234 ++++---- sites/superguidatv.it/superguidatv.it.test.js | 134 ++--- sites/taiwanplus.com/taiwanplus.com.config.js | 146 ++--- sites/taiwanplus.com/taiwanplus.com.test.js | 90 +-- sites/tapdmv.com/tapdmv.com.config.js | 122 ++-- sites/tapdmv.com/tapdmv.com.test.js | 102 ++-- .../telecablesat.fr/telecablesat.fr.config.js | 214 +++---- sites/telecablesat.fr/telecablesat.fr.test.js | 232 ++++---- sites/telenet.tv/telenet.tv.config.js | 264 ++++----- sites/telenet.tv/telenet.tv.test.js | 182 +++--- sites/teliatv.ee/teliatv.ee.config.js | 134 ++--- sites/teliatv.ee/teliatv.ee.test.js | 122 ++-- sites/telkku.com/telkku.com.config.js | 144 ++--- sites/telkku.com/telkku.com.test.js | 98 ++-- sites/telkussa.fi/telkussa.fi.config.js | 60 +- sites/telsu.fi/telsu.fi.config.js | 196 +++---- sites/telsu.fi/telsu.fi.test.js | 94 ++-- sites/tivu.tv/tivu.tv.config.js | 128 ++--- sites/tivu.tv/tivu.tv.test.js | 116 ++-- .../toonamiaftermath.com.config.js | 120 ++-- .../toonamiaftermath.com.test.js | 126 ++--- .../transvision.co.id.config.js | 186 +++--- .../transvision.co.id.test.js | 164 +++--- .../turksatkablo.com.tr.config.js | 122 ++-- .../turksatkablo.com.tr.test.js | 104 ++-- sites/tv.blue.ch/tv.blue.ch.config.js | 170 +++--- sites/tv.blue.ch/tv.blue.ch.test.js | 152 ++--- sites/tv.cctv.com/tv.cctv.com.config.js | 82 +-- sites/tv.dir.bg/tv.dir.bg.config.js | 176 +++--- sites/tv.dir.bg/tv.dir.bg.test.js | 114 ++-- sites/tv.lv/tv.lv.config.js | 86 +-- sites/tv.mail.ru/tv.mail.ru.config.js | 152 ++--- sites/tv.mail.ru/tv.mail.ru.test.js | 158 +++--- .../tv.movistar.com.pe.config.js | 114 ++-- .../tv.movistar.com.pe.test.js | 94 ++-- sites/tv.nu/tv.nu.config.js | 96 ++-- sites/tv.nu/tv.nu.test.js | 102 ++-- sites/tv.post.lu/tv.post.lu.config.js | 112 ++-- sites/tv.post.lu/tv.post.lu.test.js | 98 ++-- sites/tv.trueid.net/tv.trueid.net.config.js | 132 ++--- sites/tv.trueid.net/tv.trueid.net.test.js | 86 +-- sites/tv.vera.com.uy/tv.vera.com.uy.config.js | 216 +++---- sites/tv.vera.com.uy/tv.vera.com.uy.test.js | 176 +++--- sites/tv.yandex.ru/tv.yandex.ru.config.js | 96 ++-- sites/tv.yandex.ru/tv.yandex.ru.test.js | 112 ++-- sites/tv.yettel.hu/tv.yettel.hu.config.js | 138 ++--- sites/tv.yettel.hu/tv.yettel.hu.test.js | 166 +++--- sites/tv24.co.uk/tv24.co.uk.config.js | 184 +++--- sites/tv24.co.uk/tv24.co.uk.test.js | 106 ++-- sites/tv24.se/tv24.se.config.js | 326 +++++------ sites/tv24.se/tv24.se.test.js | 156 ++--- sites/tv2go.t-2.net/jquery.md5.js | 528 ++++++++--------- sites/tv2go.t-2.net/tv2go.t-2.net.config.js | 252 ++++----- sites/tv2go.t-2.net/tv2go.t-2.net.test.js | 140 ++--- sites/tva.tv/tva.tv.config.js | 116 ++-- sites/tva.tv/tva.tv.test.js | 100 ++-- .../tvarenasport.com.config.js | 100 ++-- .../tvarenasport.com/tvarenasport.com.test.js | 96 ++-- .../tvarenasport.hr/tvarenasport.hr.config.js | 100 ++-- sites/tvarenasport.hr/tvarenasport.hr.test.js | 96 ++-- .../tvcubana.icrt.cu.config.js | 96 ++-- .../tvcubana.icrt.cu/tvcubana.icrt.cu.test.js | 104 ++-- sites/tvgids.nl/tvgids.nl.config.js | 174 +++--- sites/tvgids.nl/tvgids.nl.test.js | 122 ++-- sites/tvguide.com/tvguide.com.config.js | 142 ++--- sites/tvguide.com/tvguide.com.test.js | 148 ++--- .../tvguide.myjcom.jp.config.js | 226 ++++---- .../tvguide.myjcom.jp.test.js | 104 ++-- sites/tvhebdo.com/tvhebdo.com.config.js | 188 +++---- sites/tvhebdo.com/tvhebdo.com.test.js | 112 ++-- sites/tvheute.at/tvheute.at.config.js | 146 ++--- sites/tvheute.at/tvheute.at.test.js | 100 ++-- sites/tvim.tv/tvim.tv.config.js | 86 +-- sites/tvim.tv/tvim.tv.test.js | 84 +-- sites/tving.com/tving.com.config.js | 188 +++---- sites/tving.com/tving.com.test.js | 100 ++-- sites/tvmi.mt/tvmi.mt.config.js | 154 ++--- sites/tvmi.mt/tvmi.mt.test.js | 106 ++-- sites/tvmusor.hu/tvmusor.hu.config.js | 162 +++--- sites/tvmusor.hu/tvmusor.hu.test.js | 142 ++--- sites/tvpassport.com/tvpassport.com.config.js | 312 +++++----- sites/tvpassport.com/tvpassport.com.test.js | 126 ++--- sites/tvplus.com.tr/tvplus.com.tr.config.js | 148 ++--- sites/tvplus.com.tr/tvplus.com.tr.test.js | 110 ++-- sites/tvprofil.com/tvprofil.com.config.js | 194 +++---- sites/tvprofil.com/tvprofil.com.test.js | 98 ++-- sites/tvtv.us/tvtv.us.config.js | 80 +-- sites/tvtv.us/tvtv.us.test.js | 108 ++-- sites/unifi.com.my/unifi.com.my.config.js | 120 ++-- sites/unifi.com.my/unifi.com.my.test.js | 114 ++-- sites/vidio.com/vidio.com.config.js | 130 ++--- sites/vidio.com/vidio.com.test.js | 110 ++-- .../virginmedia.com/virginmedia.com.config.js | 224 ++++---- sites/virginmedia.com/virginmedia.com.test.js | 312 +++++----- .../virginmediatelevision.ie.config.js | 164 +++--- .../virginmediatelevision.ie.test.js | 106 ++-- sites/visionplus.id/visionplus.id.config.js | 182 +++--- sites/visionplus.id/visionplus.id.test.js | 128 ++--- sites/vivacom.bg/vivacom.bg.config.js | 154 ++--- sites/vivacom.bg/vivacom.bg.test.js | 92 +-- sites/vtm.be/vtm.be.config.js | 96 ++-- sites/vtm.be/vtm.be.test.js | 90 +-- sites/walesi.com.fj/walesi.com.fj.config.js | 182 +++--- sites/walesi.com.fj/walesi.com.fj.test.js | 136 ++--- sites/watchyour.tv/watchyour.tv.config.js | 110 ++-- sites/watchyour.tv/watchyour.tv.test.js | 96 ++-- sites/wavve.com/wavve.com.config.js | 124 ++-- sites/wavve.com/wavve.com.test.js | 92 +-- .../worldfishingnetwork.com.config.js | 158 +++--- .../worldfishingnetwork.com.test.js | 110 ++-- sites/xumo.tv/xumo.tv.config.js | 266 ++++----- sites/xumo.tv/xumo.tv.test.js | 154 ++--- sites/zap.co.ao/zap.co.ao.config.js | 96 ++-- sites/zap.co.ao/zap.co.ao.test.js | 96 ++-- sites/ziggogo.tv/ziggogo.tv.config.js | 266 ++++----- sites/ziggogo.tv/ziggogo.tv.test.js | 196 +++---- sites/znbc.co.zm/znbc.co.zm.config.js | 130 ++--- sites/znbc.co.zm/znbc.co.zm.test.js | 110 ++-- sites/zuragt.mn/zuragt.mn.config.js | 178 +++--- sites/zuragt.mn/zuragt.mn.test.js | 98 ++-- .../channels-parse/channels-parse.config.js | 42 +- .../sites/example.com/example.com.config.js | 56 +- .../sites/example2.com/example2.com.config.js | 46 +- tests/commands/channels/editor.test.ts | 102 ++-- tests/commands/channels/lint.test.ts | 44 +- tests/commands/channels/parse.test.ts | 78 +-- tests/commands/channels/validate.test.ts | 96 ++-- tests/commands/epg/grab.test.ts | 188 +++---- 393 files changed, 28447 insertions(+), 28443 deletions(-) diff --git a/scripts/commands/api/load.ts b/scripts/commands/api/load.ts index 845b6c89e..9e0cd0a6b 100644 --- a/scripts/commands/api/load.ts +++ b/scripts/commands/api/load.ts @@ -1,18 +1,18 @@ -import { Logger } from '@freearhey/core' -import { ApiClient } from '../../core' - -async function main() { - const logger = new Logger() - const client = new ApiClient({ logger }) - - const requests = [ - client.download('channels.json'), - client.download('countries.json'), - client.download('regions.json'), - client.download('subdivisions.json') - ] - - await Promise.all(requests) -} - -main() +import { Logger } from '@freearhey/core' +import { ApiClient } from '../../core' + +async function main() { + const logger = new Logger() + const client = new ApiClient({ logger }) + + const requests = [ + client.download('channels.json'), + client.download('countries.json'), + client.download('regions.json'), + client.download('subdivisions.json') + ] + + await Promise.all(requests) +} + +main() diff --git a/scripts/commands/channels/editor.ts b/scripts/commands/channels/editor.ts index dada24c9e..d94f35a63 100644 --- a/scripts/commands/channels/editor.ts +++ b/scripts/commands/channels/editor.ts @@ -1,180 +1,180 @@ -import { DATA_DIR } from '../../constants' -import { Storage, Collection, Dictionary, Logger } from '@freearhey/core' -import { ChannelsParser, XML, ApiChannel } from '../../core' -import { Channel } from 'epg-grabber' -import { transliterate } from 'transliteration' -import nodeCleanup from 'node-cleanup' -import { program } from 'commander' -import inquirer, { QuestionCollection } from 'inquirer' - -program - .argument('', 'Path to *.channels.xml file to edit') - .option('-c, --country ', 'Default country (ISO 3166 code)', 'US') - .parse(process.argv) - -const filepath = program.args[0] -const programOptions = program.opts() -const defaultCountry = programOptions.country.toLowerCase() -const newLabel = ` [new]` - -let options = new Collection() - -async function main() { - const storage = new Storage() - - if (!(await storage.exists(filepath))) { - throw new Error(`File "${filepath}" does not exists`) - } - - const parser = new ChannelsParser({ storage }) - - const parsedChannels = await parser.parse(filepath) - options = parsedChannels.map((channel: Channel) => { - return { - channel, - delete: false - } - }) - - const dataStorage = new Storage(DATA_DIR) - const channelsContent = await dataStorage.json('channels.json') - const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) - - const buffer = new Dictionary() - options.forEach(async (option: { channel: Channel; delete: boolean }) => { - const channel = option.channel - if (channel.xmltv_id) { - if (channel.xmltv_id !== '-') { - buffer.set(`${channel.xmltv_id}/${channel.lang}`, true) - } - return - } - let choices = getOptions(channels, channel) - const question: QuestionCollection = { - name: 'option', - message: `Choose an option:`, - type: 'list', - choices, - pageSize: 10 - } - - await inquirer.prompt(question).then(async selected => { - switch (selected.option) { - case 'Overwrite': - const input = await getInput(channel) - channel.xmltv_id = input.xmltv_id - break - case 'Skip': - channel.xmltv_id = '-' - break - default: - const [, xmltv_id] = selected.option - .replace(/ \[.*\]/, '') - .split('|') - .map((i: string) => i.trim().replace(newLabel, '')) - channel.xmltv_id = xmltv_id - break - } - - const found = buffer.has(`${channel.xmltv_id}/${channel.lang}`) - if (found) { - const question: QuestionCollection = { - name: 'option', - message: `"${channel.xmltv_id}" already on the list. Choose an option:`, - type: 'list', - choices: ['Skip', 'Add', 'Delete'], - pageSize: 5 - } - await inquirer.prompt(question).then(async selected => { - switch (selected.option) { - case 'Skip': - channel.xmltv_id = '-' - break - case 'Delete': - option.delete = true - break - default: - break - } - }) - } else { - if (channel.xmltv_id !== '-') { - buffer.set(`${channel.xmltv_id}/${channel.lang}`, true) - } - } - }) - }) -} - -main() - -function save() { - const logger = new Logger() - const storage = new Storage() - - if (!storage.existsSync(filepath)) return - - const channels = options - .filter((option: { channel: Channel; delete: boolean }) => !option.delete) - .map((option: { channel: Channel; delete: boolean }) => option.channel) - - const xml = new XML(channels) - - storage.saveSync(filepath, xml.toString()) - - logger.info(`\nFile '${filepath}' successfully saved`) -} - -nodeCleanup(() => { - save() -}) - -async function getInput(channel: Channel) { - const name = channel.name.trim() - const input = await inquirer.prompt([ - { - name: 'xmltv_id', - message: ' ID:', - type: 'input', - default: generateCode(name, defaultCountry) - } - ]) - - return { name, xmltv_id: input['xmltv_id'] } -} - -function getOptions(channels: Collection, channel: Channel) { - const channelId = generateCode(channel.name, defaultCountry) - const similar = getSimilar(channels, channelId) - - const variants = new Collection() - variants.add(`${channel.name.trim()} | ${channelId}${newLabel}`) - similar.forEach((_channel: ApiChannel) => { - const altNames = _channel.altNames.notEmpty() ? ` (${_channel.altNames.join(',')})` : '' - const closed = _channel.closed ? `[closed:${_channel.closed}]` : `` - const replacedBy = _channel.replacedBy ? `[replaced_by:${_channel.replacedBy}]` : '' - - variants.add(`${_channel.name}${altNames} | ${_channel.id} ${closed}${replacedBy}[api]`) - }) - variants.add(`Overwrite`) - variants.add(`Skip`) - - return variants.all() -} - -function getSimilar(channels: Collection, channelId: string) { - const normChannelId = channelId.split('.')[0].slice(0, 8).toLowerCase() - - return channels.filter((channel: ApiChannel) => - channel.id.split('.')[0].toLowerCase().startsWith(normChannelId) - ) -} - -function generateCode(name: string, country: string) { - const channelId: string = transliterate(name) - .replace(/\+/gi, 'Plus') - .replace(/^\&/gi, 'And') - .replace(/[^a-z\d]+/gi, '') - - return `${channelId}.${country}` -} +import { DATA_DIR } from '../../constants' +import { Storage, Collection, Dictionary, Logger } from '@freearhey/core' +import { ChannelsParser, XML, ApiChannel } from '../../core' +import { Channel } from 'epg-grabber' +import { transliterate } from 'transliteration' +import nodeCleanup from 'node-cleanup' +import { program } from 'commander' +import inquirer, { QuestionCollection } from 'inquirer' + +program + .argument('', 'Path to *.channels.xml file to edit') + .option('-c, --country ', 'Default country (ISO 3166 code)', 'US') + .parse(process.argv) + +const filepath = program.args[0] +const programOptions = program.opts() +const defaultCountry = programOptions.country.toLowerCase() +const newLabel = ' [new]' + +let options = new Collection() + +async function main() { + const storage = new Storage() + + if (!(await storage.exists(filepath))) { + throw new Error(`File "${filepath}" does not exists`) + } + + const parser = new ChannelsParser({ storage }) + + const parsedChannels = await parser.parse(filepath) + options = parsedChannels.map((channel: Channel) => { + return { + channel, + delete: false + } + }) + + const dataStorage = new Storage(DATA_DIR) + const channelsContent = await dataStorage.json('channels.json') + const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) + + const buffer = new Dictionary() + options.forEach(async (option: { channel: Channel; delete: boolean }) => { + const channel = option.channel + if (channel.xmltv_id) { + if (channel.xmltv_id !== '-') { + buffer.set(`${channel.xmltv_id}/${channel.lang}`, true) + } + return + } + const choices = getOptions(channels, channel) + const question: QuestionCollection = { + name: 'option', + message: 'Choose an option:', + type: 'list', + choices, + pageSize: 10 + } + + await inquirer.prompt(question).then(async selected => { + switch (selected.option) { + case 'Overwrite': + const input = await getInput(channel) + channel.xmltv_id = input.xmltv_id + break + case 'Skip': + channel.xmltv_id = '-' + break + default: + const [, xmltv_id] = selected.option + .replace(/ \[.*\]/, '') + .split('|') + .map((i: string) => i.trim().replace(newLabel, '')) + channel.xmltv_id = xmltv_id + break + } + + const found = buffer.has(`${channel.xmltv_id}/${channel.lang}`) + if (found) { + const question: QuestionCollection = { + name: 'option', + message: `"${channel.xmltv_id}" already on the list. Choose an option:`, + type: 'list', + choices: ['Skip', 'Add', 'Delete'], + pageSize: 5 + } + await inquirer.prompt(question).then(async selected => { + switch (selected.option) { + case 'Skip': + channel.xmltv_id = '-' + break + case 'Delete': + option.delete = true + break + default: + break + } + }) + } else { + if (channel.xmltv_id !== '-') { + buffer.set(`${channel.xmltv_id}/${channel.lang}`, true) + } + } + }) + }) +} + +main() + +function save() { + const logger = new Logger() + const storage = new Storage() + + if (!storage.existsSync(filepath)) return + + const channels = options + .filter((option: { channel: Channel; delete: boolean }) => !option.delete) + .map((option: { channel: Channel; delete: boolean }) => option.channel) + + const xml = new XML(channels) + + storage.saveSync(filepath, xml.toString()) + + logger.info(`\nFile '${filepath}' successfully saved`) +} + +nodeCleanup(() => { + save() +}) + +async function getInput(channel: Channel) { + const name = channel.name.trim() + const input = await inquirer.prompt([ + { + name: 'xmltv_id', + message: ' ID:', + type: 'input', + default: generateCode(name, defaultCountry) + } + ]) + + return { name, xmltv_id: input['xmltv_id'] } +} + +function getOptions(channels: Collection, channel: Channel) { + const channelId = generateCode(channel.name, defaultCountry) + const similar = getSimilar(channels, channelId) + + const variants = new Collection() + variants.add(`${channel.name.trim()} | ${channelId}${newLabel}`) + similar.forEach((_channel: ApiChannel) => { + const altNames = _channel.altNames.notEmpty() ? ` (${_channel.altNames.join(',')})` : '' + const closed = _channel.closed ? `[closed:${_channel.closed}]` : '' + const replacedBy = _channel.replacedBy ? `[replaced_by:${_channel.replacedBy}]` : '' + + variants.add(`${_channel.name}${altNames} | ${_channel.id} ${closed}${replacedBy}[api]`) + }) + variants.add('Overwrite') + variants.add('Skip') + + return variants.all() +} + +function getSimilar(channels: Collection, channelId: string) { + const normChannelId = channelId.split('.')[0].slice(0, 8).toLowerCase() + + return channels.filter((channel: ApiChannel) => + channel.id.split('.')[0].toLowerCase().startsWith(normChannelId) + ) +} + +function generateCode(name: string, country: string) { + const channelId: string = transliterate(name) + .replace(/\+/gi, 'Plus') + .replace(/^&/gi, 'And') + .replace(/[^a-z\d]+/gi, '') + + return `${channelId}.${country}` +} diff --git a/scripts/commands/channels/lint.ts b/scripts/commands/channels/lint.ts index e486f9d1a..37c6b646c 100644 --- a/scripts/commands/channels/lint.ts +++ b/scripts/commands/channels/lint.ts @@ -1,78 +1,78 @@ -import chalk from 'chalk' -import libxml, { ValidationError } from 'libxmljs2' -import { program } from 'commander' -import { Logger, Storage, File } from '@freearhey/core' - -const xsd = ` - - - - - - - - - - - - - - - - - -` - -program - .option( - '-c, --channels ', - 'Path to channels.xml file to validate', - 'sites/**/*.channels.xml' - ) - .parse(process.argv) - -const options = program.opts() - -async function main() { - const logger = new Logger() - const storage = new Storage() - - logger.info('options:') - logger.tree(options) - - let errors: ValidationError[] = [] - - let files: string[] = await storage.list(options.channels) - for (const filepath of files) { - const file = new File(filepath) - if (file.extension() !== 'xml') continue - - const xml = await storage.load(filepath) - - let localErrors: ValidationError[] = [] - - const xsdDoc = libxml.parseXml(xsd) - const doc = libxml.parseXml(xml) - - if (!doc.validate(xsdDoc)) { - localErrors = doc.validationErrors - } - - if (localErrors.length) { - console.log(`\n${chalk.underline(filepath)}`) - localErrors.forEach((error: ValidationError) => { - const position = `${error.line}:${error.column}` - console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`) - }) - - errors = errors.concat(localErrors) - } - } - - if (errors.length) { - console.log(chalk.red(`\n${errors.length} error(s)`)) - process.exit(1) - } -} - -main() +import chalk from 'chalk' +import libxml, { ValidationError } from 'libxmljs2' +import { program } from 'commander' +import { Logger, Storage, File } from '@freearhey/core' + +const xsd = ` + + + + + + + + + + + + + + + + + +` + +program + .option( + '-c, --channels ', + 'Path to channels.xml file to validate', + 'sites/**/*.channels.xml' + ) + .parse(process.argv) + +const options = program.opts() + +async function main() { + const logger = new Logger() + const storage = new Storage() + + logger.info('options:') + logger.tree(options) + + let errors: ValidationError[] = [] + + const files: string[] = await storage.list(options.channels) + for (const filepath of files) { + const file = new File(filepath) + if (file.extension() !== 'xml') continue + + const xml = await storage.load(filepath) + + let localErrors: ValidationError[] = [] + + const xsdDoc = libxml.parseXml(xsd) + const doc = libxml.parseXml(xml) + + if (!doc.validate(xsdDoc)) { + localErrors = doc.validationErrors + } + + if (localErrors.length) { + console.log(`\n${chalk.underline(filepath)}`) + localErrors.forEach((error: ValidationError) => { + const position = `${error.line}:${error.column}` + console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`) + }) + + errors = errors.concat(localErrors) + } + } + + if (errors.length) { + console.log(chalk.red(`\n${errors.length} error(s)`)) + process.exit(1) + } +} + +main() diff --git a/scripts/commands/channels/parse.ts b/scripts/commands/channels/parse.ts index 31f801251..c91856526 100644 --- a/scripts/commands/channels/parse.ts +++ b/scripts/commands/channels/parse.ts @@ -1,81 +1,85 @@ -import { Logger, File, Collection, Storage } from '@freearhey/core' -import { ChannelsParser, XML } from '../../core' -import { Channel } from 'epg-grabber' -import { Command, OptionValues } from 'commander' -import path from 'path' - -const program = new Command() -program - .requiredOption('-c, --config ', 'Config file') - .option('-s, --set [args...]', 'Set custom arguments') - .option('-o, --output ', 'Output file') - .option('--clean', 'Delete the previous *.channels.xml if exists') - .parse(process.argv) - -type ParseOptions = { - config: string - set?: string - output?: string - clean?: boolean -} - -const options: ParseOptions = program.opts() - -async function main() { - const storage = new Storage() - const parser = new ChannelsParser({ storage }) - const logger = new Logger() - const file = new File(options.config) - const dir = file.dirname() - const config = require(path.resolve(options.config)) - const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` - - let channels = new Collection() - if (!options.clean && (await storage.exists(outputFilepath))) { - channels = await parser.parse(outputFilepath) - } - - const args: { - [key: string]: any - } = {} - - if (Array.isArray(options.set)) { - options.set.forEach((arg: string) => { - const [key, value] = arg.split(':') - args[key] = value - }) - } - - let parsedChannels = config.channels(args) - if (isPromise(parsedChannels)) { - parsedChannels = await parsedChannels - } - parsedChannels = parsedChannels.map((channel: Channel) => { - channel.site = config.site - - return channel - }) - - channels = channels - .mergeBy( - new Collection(parsedChannels), - (channel: Channel) => channel.site_id.toString() + channel.lang - ) - .orderBy([ - (channel: Channel) => channel.lang, - (channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '_'), - (channel: Channel) => channel.site_id - ]) - - const xml = new XML(channels) - - await storage.save(outputFilepath, xml.toString()) - - logger.info(`File '${outputFilepath}' successfully saved`) -} - -main() - -function isPromise(promise: any) { - return !!promise && typeof promise.then === 'function' -} +import { Logger, File, Collection, Storage } from '@freearhey/core' +import { ChannelsParser, XML } from '../../core' +import { Channel } from 'epg-grabber' +import { Command } from 'commander' +import path from 'path' + +const program = new Command() +program + .requiredOption('-c, --config ', 'Config file') + .option('-s, --set [args...]', 'Set custom arguments') + .option('-o, --output ', 'Output file') + .option('--clean', 'Delete the previous *.channels.xml if exists') + .parse(process.argv) + +type ParseOptions = { + config: string + set?: string + output?: string + clean?: boolean +} + +const options: ParseOptions = program.opts() + +async function main() { + const storage = new Storage() + const parser = new ChannelsParser({ storage }) + const logger = new Logger() + const file = new File(options.config) + const dir = file.dirname() + const config = require(path.resolve(options.config)) + const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` + + let channels = new Collection() + if (!options.clean && (await storage.exists(outputFilepath))) { + channels = await parser.parse(outputFilepath) + } + + const args: { + [key: string]: string + } = {} + + if (Array.isArray(options.set)) { + options.set.forEach((arg: string) => { + const [key, value] = arg.split(':') + args[key] = value + }) + } + + let parsedChannels = config.channels(args) + if (isPromise(parsedChannels)) { + parsedChannels = await parsedChannels + } + parsedChannels = parsedChannels.map((channel: Channel) => { + channel.site = config.site + + return channel + }) + + channels = channels + .mergeBy( + new Collection(parsedChannels), + (channel: Channel) => channel.site_id.toString() + channel.lang + ) + .orderBy([ + (channel: Channel) => channel.lang, + (channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '_'), + (channel: Channel) => channel.site_id + ]) + + const xml = new XML(channels) + + await storage.save(outputFilepath, xml.toString()) + + logger.info(`File '${outputFilepath}' successfully saved`) +} + +main() + +function isPromise(promise: object[] | Promise) { + return ( + !!promise && + typeof promise === 'object' && + typeof (promise as Promise).then === 'function' + ) +} diff --git a/scripts/commands/channels/validate.ts b/scripts/commands/channels/validate.ts index acda8863c..2a057aac7 100644 --- a/scripts/commands/channels/validate.ts +++ b/scripts/commands/channels/validate.ts @@ -1,95 +1,95 @@ -import { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core' -import { ChannelsParser, ApiChannel } from '../../core' -import { program } from 'commander' -import chalk from 'chalk' -import langs from 'langs' -import { DATA_DIR } from '../../constants' -import { Channel } from 'epg-grabber' - -program - .option( - '-c, --channels ', - 'Path to channels.xml file to validate', - 'sites/**/*.channels.xml' - ) - .parse(process.argv) - -const options = program.opts() - -type ValidationError = { - type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang' - name: string - lang?: string - xmltv_id?: string - site_id?: string - logo?: string -} - -async function main() { - const logger = new Logger() - - logger.info('options:') - logger.tree(options) - - const parser = new ChannelsParser({ storage: new Storage() }) - - const dataStorage = new Storage(DATA_DIR) - const channelsContent = await dataStorage.json('channels.json') - const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) - - let totalFiles = 0 - let totalErrors = 0 - const storage = new Storage() - let files: string[] = await storage.list(options.channels) - for (const filepath of files) { - const file = new File(filepath) - if (file.extension() !== 'xml') continue - - const parsedChannels = await parser.parse(filepath) - - const bufferById = new Dictionary() - const bufferBySiteId = new Dictionary() - const errors: ValidationError[] = [] - parsedChannels.forEach((channel: Channel) => { - const bufferId: string = `${channel.xmltv_id}:${channel.lang}` - if (bufferById.missing(bufferId)) { - bufferById.set(bufferId, true) - } else { - errors.push({ type: 'duplicate', ...channel }) - totalErrors++ - } - - const bufferSiteId: string = `${channel.site_id}:${channel.lang}` - if (bufferBySiteId.missing(bufferSiteId)) { - bufferBySiteId.set(bufferSiteId, true) - } else { - errors.push({ type: 'duplicate', ...channel }) - totalErrors++ - } - - if (channels.missing((_channel: ApiChannel) => _channel.id === channel.xmltv_id)) { - errors.push({ type: 'wrong_xmltv_id', ...channel }) - totalErrors++ - } - - if (!langs.where('1', channel.lang)) { - errors.push({ type: 'wrong_lang', ...channel }) - totalErrors++ - } - }) - - if (errors.length) { - console.log(chalk.underline(filepath)) - console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name']) - console.log() - totalFiles++ - } - } - - if (totalErrors > 0) { - console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`)) - process.exit(1) - } -} - -main() +import { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core' +import { ChannelsParser, ApiChannel } from '../../core' +import { program } from 'commander' +import chalk from 'chalk' +import langs from 'langs' +import { DATA_DIR } from '../../constants' +import { Channel } from 'epg-grabber' + +program + .option( + '-c, --channels ', + 'Path to channels.xml file to validate', + 'sites/**/*.channels.xml' + ) + .parse(process.argv) + +const options = program.opts() + +type ValidationError = { + type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang' + name: string + lang?: string + xmltv_id?: string + site_id?: string + logo?: string +} + +async function main() { + const logger = new Logger() + + logger.info('options:') + logger.tree(options) + + const parser = new ChannelsParser({ storage: new Storage() }) + + const dataStorage = new Storage(DATA_DIR) + const channelsContent = await dataStorage.json('channels.json') + const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) + + let totalFiles = 0 + let totalErrors = 0 + const storage = new Storage() + const files: string[] = await storage.list(options.channels) + for (const filepath of files) { + const file = new File(filepath) + if (file.extension() !== 'xml') continue + + const parsedChannels = await parser.parse(filepath) + + const bufferById = new Dictionary() + const bufferBySiteId = new Dictionary() + const errors: ValidationError[] = [] + parsedChannels.forEach((channel: Channel) => { + const bufferId: string = `${channel.xmltv_id}:${channel.lang}` + if (bufferById.missing(bufferId)) { + bufferById.set(bufferId, true) + } else { + errors.push({ type: 'duplicate', ...channel }) + totalErrors++ + } + + const bufferSiteId: string = `${channel.site_id}:${channel.lang}` + if (bufferBySiteId.missing(bufferSiteId)) { + bufferBySiteId.set(bufferSiteId, true) + } else { + errors.push({ type: 'duplicate', ...channel }) + totalErrors++ + } + + if (channels.missing((_channel: ApiChannel) => _channel.id === channel.xmltv_id)) { + errors.push({ type: 'wrong_xmltv_id', ...channel }) + totalErrors++ + } + + if (!langs.where('1', channel.lang)) { + errors.push({ type: 'wrong_lang', ...channel }) + totalErrors++ + } + }) + + if (errors.length) { + console.log(chalk.underline(filepath)) + console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name']) + console.log() + totalFiles++ + } + } + + if (totalErrors > 0) { + console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`)) + process.exit(1) + } +} + +main() diff --git a/scripts/commands/epg/grab.ts b/scripts/commands/epg/grab.ts index 9741c8d7f..cc0d03f2c 100644 --- a/scripts/commands/epg/grab.ts +++ b/scripts/commands/epg/grab.ts @@ -1,117 +1,117 @@ -import { Logger, Timer, Storage, Collection } from '@freearhey/core' -import { program } from 'commander' -import { CronJob } from 'cron' -import { QueueCreator, Job, ChannelsParser } from '../../core' -import { Channel } from 'epg-grabber' -import path from 'path' -import { SITES_DIR } from '../../constants' - -program - .option('-s, --site ', 'Name of the site to parse') - .option( - '-c, --channels ', - 'Path to *.channels.xml file (required if the "--site" attribute is not specified)' - ) - .option('-o, --output ', 'Path to output file', 'guide.xml') - .option('-l, --lang ', 'Filter channels by language (ISO 639-2 code)') - .option('-t, --timeout ', 'Override the default timeout for each request') - .option( - '--days ', - 'Override the number of days for which the program will be loaded (defaults to the value from the site config)', - value => parseInt(value) - ) - .option( - '--maxConnections ', - 'Limit on the number of concurrent requests', - value => parseInt(value), - 1 - ) - .option('--cron ', 'Schedule a script run (example: "0 0 * * *")') - .option('--gzip', 'Create a compressed version of the guide as well', false) - .parse(process.argv) - -export type GrabOptions = { - site?: string - channels?: string - output: string - gzip: boolean - maxConnections: number - timeout?: string - lang?: string - days?: number - cron?: string -} - -const options: GrabOptions = program.opts() - -async function main() { - if (!options.site && !options.channels) - throw new Error('One of the arguments must be presented: `--site` or `--channels`') - - const logger = new Logger() - - logger.start('staring...') - - logger.info('config:') - logger.tree(options) - - logger.info(`loading channels...`) - const storage = new Storage() - const parser = new ChannelsParser({ storage }) - - let files: string[] = [] - if (options.site) { - let pattern = path.join(SITES_DIR, options.site, '*.channels.xml') - pattern = pattern.replace(/\\/g, '/') - files = await storage.list(pattern) - } else if (options.channels) { - files = await storage.list(options.channels) - } - - let parsedChannels = new Collection() - for (let filepath of files) { - parsedChannels = parsedChannels.concat(await parser.parse(filepath)) - } - if (options.lang) { - parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang) - } - logger.info(` found ${parsedChannels.count()} channels`) - - logger.info('creating queue...') - const queueCreator = new QueueCreator({ - parsedChannels, - logger, - options - }) - const queue = await queueCreator.create() - logger.info(` added ${queue.size()} items`) - - const job = new Job({ - queue, - logger, - options - }) - - let runIndex = 1 - if (options.cron) { - const cronJob = new CronJob(options.cron, async () => { - logger.info(`run #${runIndex}:`) - const timer = new Timer() - timer.start() - await job.run() - runIndex++ - logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) - }) - cronJob.start() - } else { - logger.info(`run #${runIndex}:`) - const timer = new Timer() - timer.start() - await job.run() - logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) - } - - logger.info('finished') -} - -main() +import { Logger, Timer, Storage, Collection } from '@freearhey/core' +import { program } from 'commander' +import { CronJob } from 'cron' +import { QueueCreator, Job, ChannelsParser } from '../../core' +import { Channel } from 'epg-grabber' +import path from 'path' +import { SITES_DIR } from '../../constants' + +program + .option('-s, --site ', 'Name of the site to parse') + .option( + '-c, --channels ', + 'Path to *.channels.xml file (required if the "--site" attribute is not specified)' + ) + .option('-o, --output ', 'Path to output file', 'guide.xml') + .option('-l, --lang ', 'Filter channels by language (ISO 639-2 code)') + .option('-t, --timeout ', 'Override the default timeout for each request') + .option( + '--days ', + 'Override the number of days for which the program will be loaded (defaults to the value from the site config)', + value => parseInt(value) + ) + .option( + '--maxConnections ', + 'Limit on the number of concurrent requests', + value => parseInt(value), + 1 + ) + .option('--cron ', 'Schedule a script run (example: "0 0 * * *")') + .option('--gzip', 'Create a compressed version of the guide as well', false) + .parse(process.argv) + +export type GrabOptions = { + site?: string + channels?: string + output: string + gzip: boolean + maxConnections: number + timeout?: string + lang?: string + days?: number + cron?: string +} + +const options: GrabOptions = program.opts() + +async function main() { + if (!options.site && !options.channels) + throw new Error('One of the arguments must be presented: `--site` or `--channels`') + + const logger = new Logger() + + logger.start('staring...') + + logger.info('config:') + logger.tree(options) + + logger.info('loading channels...') + const storage = new Storage() + const parser = new ChannelsParser({ storage }) + + let files: string[] = [] + if (options.site) { + let pattern = path.join(SITES_DIR, options.site, '*.channels.xml') + pattern = pattern.replace(/\\/g, '/') + files = await storage.list(pattern) + } else if (options.channels) { + files = await storage.list(options.channels) + } + + let parsedChannels = new Collection() + for (const filepath of files) { + parsedChannels = parsedChannels.concat(await parser.parse(filepath)) + } + if (options.lang) { + parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang) + } + logger.info(` found ${parsedChannels.count()} channels`) + + logger.info('creating queue...') + const queueCreator = new QueueCreator({ + parsedChannels, + logger, + options + }) + const queue = await queueCreator.create() + logger.info(` added ${queue.size()} items`) + + const job = new Job({ + queue, + logger, + options + }) + + let runIndex = 1 + if (options.cron) { + const cronJob = new CronJob(options.cron, async () => { + logger.info(`run #${runIndex}:`) + const timer = new Timer() + timer.start() + await job.run() + runIndex++ + logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) + }) + cronJob.start() + } else { + logger.info(`run #${runIndex}:`) + const timer = new Timer() + timer.start() + await job.run() + logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) + } + + logger.info('finished') +} + +main() diff --git a/scripts/constants.ts b/scripts/constants.ts index 7b6c00d0d..25231ce08 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -1,4 +1,4 @@ -export const SITES_DIR = process.env.SITES_DIR || './sites' -export const GUIDES_DIR = process.env.GUIDES_DIR || './guides' -export const DATA_DIR = process.env.DATA_DIR || './temp/data' -export const CURR_DATE = process.env.CURR_DATE || new Date().toISOString() +export const SITES_DIR = process.env.SITES_DIR || './sites' +export const GUIDES_DIR = process.env.GUIDES_DIR || './guides' +export const DATA_DIR = process.env.DATA_DIR || './temp/data' +export const CURR_DATE = process.env.CURR_DATE || new Date().toISOString() diff --git a/scripts/core/apiChannel.ts b/scripts/core/apiChannel.ts index 3ddd1a910..fd1fcd696 100644 --- a/scripts/core/apiChannel.ts +++ b/scripts/core/apiChannel.ts @@ -1,79 +1,79 @@ -import { Collection } from '@freearhey/core' - -type ApiChannelProps = { - id: string - name: string - alt_names: string[] - network: string - owners: string[] - country: string - subdivision: string - city: string - broadcast_area: string[] - languages: string[] - categories: string[] - is_nsfw: boolean - launched: string - closed: string - replaced_by: string - website: string - logo: string -} - -export class ApiChannel { - id: string - name: string - altNames: Collection - network: string - owners: Collection - country: string - subdivision: string - city: string - broadcastArea: Collection - languages: Collection - categories: Collection - isNSFW: boolean - launched: string - closed: string - replacedBy: string - website: string - logo: string - - constructor({ - id, - name, - alt_names, - network, - owners, - country, - subdivision, - city, - broadcast_area, - languages, - categories, - is_nsfw, - launched, - closed, - replaced_by, - website, - logo - }: ApiChannelProps) { - this.id = id - this.name = name - this.altNames = new Collection(alt_names) - this.network = network - this.owners = new Collection(owners) - this.country = country - this.subdivision = subdivision - this.city = city - this.broadcastArea = new Collection(broadcast_area) - this.languages = new Collection(languages) - this.categories = new Collection(categories) - this.isNSFW = is_nsfw - this.launched = launched - this.closed = closed - this.replacedBy = replaced_by - this.website = website - this.logo = logo - } -} +import { Collection } from '@freearhey/core' + +type ApiChannelProps = { + id: string + name: string + alt_names: string[] + network: string + owners: string[] + country: string + subdivision: string + city: string + broadcast_area: string[] + languages: string[] + categories: string[] + is_nsfw: boolean + launched: string + closed: string + replaced_by: string + website: string + logo: string +} + +export class ApiChannel { + id: string + name: string + altNames: Collection + network: string + owners: Collection + country: string + subdivision: string + city: string + broadcastArea: Collection + languages: Collection + categories: Collection + isNSFW: boolean + launched: string + closed: string + replacedBy: string + website: string + logo: string + + constructor({ + id, + name, + alt_names, + network, + owners, + country, + subdivision, + city, + broadcast_area, + languages, + categories, + is_nsfw, + launched, + closed, + replaced_by, + website, + logo + }: ApiChannelProps) { + this.id = id + this.name = name + this.altNames = new Collection(alt_names) + this.network = network + this.owners = new Collection(owners) + this.country = country + this.subdivision = subdivision + this.city = city + this.broadcastArea = new Collection(broadcast_area) + this.languages = new Collection(languages) + this.categories = new Collection(categories) + this.isNSFW = is_nsfw + this.launched = launched + this.closed = closed + this.replacedBy = replaced_by + this.website = website + this.logo = logo + } +} diff --git a/scripts/core/apiClient.ts b/scripts/core/apiClient.ts index 66fa28a87..2db0f3d0f 100644 --- a/scripts/core/apiClient.ts +++ b/scripts/core/apiClient.ts @@ -1,59 +1,59 @@ -import { Logger, Storage } from '@freearhey/core' -import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios' -import cliProgress, { MultiBar } from 'cli-progress' -import numeral from 'numeral' - -export class ApiClient { - progressBar: MultiBar - client: AxiosInstance - storage: Storage - logger: Logger - - constructor({ logger }: { logger: Logger }) { - this.logger = logger - this.client = axios.create({ - responseType: 'stream' - }) - this.storage = new Storage() - this.progressBar = new cliProgress.MultiBar({ - stopOnComplete: true, - hideCursor: true, - forceRedraw: true, - barsize: 36, - format(options, params, payload) { - const filename = payload.filename.padEnd(18, ' ') - const barsize = options.barsize || 40 - const percent = (params.progress * 100).toFixed(2) - const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A' - const total = numeral(params.total).format('0.0 b') - const completeSize = Math.round(params.progress * barsize) - const incompleteSize = barsize - completeSize - const bar = - options.barCompleteString && options.barIncompleteString - ? options.barCompleteString.substr(0, completeSize) + - options.barGlue + - options.barIncompleteString.substr(0, incompleteSize) - : '-'.repeat(barsize) - - return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}` - } - }) - } - - async download(filename: string) { - const stream = await this.storage.createStream(`/temp/data/${filename}`) - - const bar = this.progressBar.create(0, 0, { filename }) - - this.client - .get(`https://iptv-org.github.io/api/${filename}`, { - onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) { - if (total) bar.setTotal(total) - bar.update(loaded, { speed: rate }) - } - }) - .then((response: AxiosResponse) => { - response.data.pipe(stream) - }) - } -} +import { Logger, Storage } from '@freearhey/core' +import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios' +import cliProgress, { MultiBar } from 'cli-progress' +import numeral from 'numeral' + +export class ApiClient { + progressBar: MultiBar + client: AxiosInstance + storage: Storage + logger: Logger + + constructor({ logger }: { logger: Logger }) { + this.logger = logger + this.client = axios.create({ + responseType: 'stream' + }) + this.storage = new Storage() + this.progressBar = new cliProgress.MultiBar({ + stopOnComplete: true, + hideCursor: true, + forceRedraw: true, + barsize: 36, + format(options, params, payload) { + const filename = payload.filename.padEnd(18, ' ') + const barsize = options.barsize || 40 + const percent = (params.progress * 100).toFixed(2) + const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A' + const total = numeral(params.total).format('0.0 b') + const completeSize = Math.round(params.progress * barsize) + const incompleteSize = barsize - completeSize + const bar = + options.barCompleteString && options.barIncompleteString + ? options.barCompleteString.substr(0, completeSize) + + options.barGlue + + options.barIncompleteString.substr(0, incompleteSize) + : '-'.repeat(barsize) + + return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}` + } + }) + } + + async download(filename: string) { + const stream = await this.storage.createStream(`/temp/data/${filename}`) + + const bar = this.progressBar.create(0, 0, { filename }) + + this.client + .get(`https://iptv-org.github.io/api/${filename}`, { + onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) { + if (total) bar.setTotal(total) + bar.update(loaded, { speed: rate }) + } + }) + .then((response: AxiosResponse) => { + response.data.pipe(stream) + }) + } +} diff --git a/scripts/core/channelsParser.ts b/scripts/core/channelsParser.ts index 990422b4b..d46305066 100644 --- a/scripts/core/channelsParser.ts +++ b/scripts/core/channelsParser.ts @@ -1,24 +1,24 @@ -import { parseChannels } from 'epg-grabber' -import { Storage, Collection } from '@freearhey/core' - -type ChannelsParserProps = { - storage: Storage -} - -export class ChannelsParser { - storage: Storage - - constructor({ storage }: ChannelsParserProps) { - this.storage = storage - } - - async parse(filepath: string) { - let parsedChannels = new Collection() - - const content = await this.storage.load(filepath) - const channels = parseChannels(content) - parsedChannels = parsedChannels.concat(new Collection(channels)) - - return parsedChannels - } -} +import { parseChannels } from 'epg-grabber' +import { Storage, Collection } from '@freearhey/core' + +type ChannelsParserProps = { + storage: Storage +} + +export class ChannelsParser { + storage: Storage + + constructor({ storage }: ChannelsParserProps) { + this.storage = storage + } + + async parse(filepath: string) { + let parsedChannels = new Collection() + + const content = await this.storage.load(filepath) + const channels = parseChannels(content) + parsedChannels = parsedChannels.concat(new Collection(channels)) + + return parsedChannels + } +} diff --git a/scripts/core/configLoader.ts b/scripts/core/configLoader.ts index 2cff65d8f..31c29c760 100644 --- a/scripts/core/configLoader.ts +++ b/scripts/core/configLoader.ts @@ -1,21 +1,21 @@ -import { SiteConfig } from 'epg-grabber' -import _ from 'lodash' -import { pathToFileURL } from 'url' - -export class ConfigLoader { - async load(filepath: string): Promise { - const fileUrl = pathToFileURL(filepath).toString() - const config = (await import(fileUrl)).default - - return _.merge( - { - delay: 0, - maxConnections: 1, - request: { - timeout: 30000 - } - }, - config - ) - } -} +import { SiteConfig } from 'epg-grabber' +import _ from 'lodash' +import { pathToFileURL } from 'url' + +export class ConfigLoader { + async load(filepath: string): Promise { + const fileUrl = pathToFileURL(filepath).toString() + const config = (await import(fileUrl)).default + + return _.merge( + { + delay: 0, + maxConnections: 1, + request: { + timeout: 30000 + } + }, + config + ) + } +} diff --git a/scripts/core/date.js b/scripts/core/date.js index 4dff51990..5515a1c48 100644 --- a/scripts/core/date.js +++ b/scripts/core/date.js @@ -1,13 +1,13 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = {} - -date.getUTC = function (d = null) { - if (typeof d === 'string') return dayjs.utc(d).startOf('d') - - return dayjs.utc().startOf('d') -} - -module.exports = date +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = {} + +date.getUTC = function (d = null) { + if (typeof d === 'string') return dayjs.utc(d).startOf('d') + + return dayjs.utc().startOf('d') +} + +module.exports = date diff --git a/scripts/core/grabber.ts b/scripts/core/grabber.ts index 783e3f148..c91d66ffa 100644 --- a/scripts/core/grabber.ts +++ b/scripts/core/grabber.ts @@ -1,75 +1,75 @@ -import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber' -import { Logger, Collection } from '@freearhey/core' -import { Queue } from './' -import { GrabOptions } from '../commands/epg/grab' -import { TaskQueue, PromisyClass } from 'cwait' - -type GrabberProps = { - logger: Logger - queue: Queue - options: GrabOptions -} - -export class Grabber { - logger: Logger - queue: Queue - options: GrabOptions - - constructor({ logger, queue, options }: GrabberProps) { - this.logger = logger - this.queue = queue - this.options = options - } - - async grab(): Promise<{ channels: Collection; programs: Collection }> { - const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections) - - const total = this.queue.size() - - const channels = new Collection() - let programs = new Collection() - let i = 1 - - await Promise.all( - this.queue.items().map( - taskQueue.wrap( - async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => { - const { channel, config, date } = queueItem - - channels.add(channel) - - if (this.options.timeout !== undefined) { - const timeout = parseInt(this.options.timeout) - config.request = { ...config.request, ...{ timeout } } - } - - const grabber = - process.env.NODE_ENV === 'test' ? new EPGGrabberMock(config) : new EPGGrabber(config) - const _programs = await grabber.grab( - channel, - date, - (data: GrabCallbackData, error: Error | null) => { - const { programs, date } = data - - this.logger.info( - ` [${i}/${total}] ${channel.site} (${channel.lang}) - ${ - channel.xmltv_id - } - ${date.format('MMM D, YYYY')} (${programs.length} programs)` - ) - if (i < total) i++ - - if (error) { - this.logger.info(` ERR: ${error.message}`) - } - } - ) - - programs = programs.concat(new Collection(_programs)) - } - ) - ) - ) - - return { channels, programs } - } -} +import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber' +import { Logger, Collection } from '@freearhey/core' +import { Queue } from './' +import { GrabOptions } from '../commands/epg/grab' +import { TaskQueue, PromisyClass } from 'cwait' + +type GrabberProps = { + logger: Logger + queue: Queue + options: GrabOptions +} + +export class Grabber { + logger: Logger + queue: Queue + options: GrabOptions + + constructor({ logger, queue, options }: GrabberProps) { + this.logger = logger + this.queue = queue + this.options = options + } + + async grab(): Promise<{ channels: Collection; programs: Collection }> { + const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections) + + const total = this.queue.size() + + const channels = new Collection() + let programs = new Collection() + let i = 1 + + await Promise.all( + this.queue.items().map( + taskQueue.wrap( + async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => { + const { channel, config, date } = queueItem + + channels.add(channel) + + if (this.options.timeout !== undefined) { + const timeout = parseInt(this.options.timeout) + config.request = { ...config.request, ...{ timeout } } + } + + const grabber = + process.env.NODE_ENV === 'test' ? new EPGGrabberMock(config) : new EPGGrabber(config) + const _programs = await grabber.grab( + channel, + date, + (data: GrabCallbackData, error: Error | null) => { + const { programs, date } = data + + this.logger.info( + ` [${i}/${total}] ${channel.site} (${channel.lang}) - ${ + channel.xmltv_id + } - ${date.format('MMM D, YYYY')} (${programs.length} programs)` + ) + if (i < total) i++ + + if (error) { + this.logger.info(` ERR: ${error.message}`) + } + } + ) + + programs = programs.concat(new Collection(_programs)) + } + ) + ) + ) + + return { channels, programs } + } +} diff --git a/scripts/core/guide.ts b/scripts/core/guide.ts index 2b73f05ea..3b6804c92 100644 --- a/scripts/core/guide.ts +++ b/scripts/core/guide.ts @@ -1,55 +1,55 @@ -import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core' -import { Channel } from 'epg-grabber' -import { XMLTV } from '../core' -import { CURR_DATE } from '../constants' - -type GuideProps = { - channels: Collection - programs: Collection - logger: Logger - filepath: string - gzip: boolean -} - -export class Guide { - channels: Collection - programs: Collection - logger: Logger - storage: Storage - filepath: string - gzip: boolean - - constructor({ channels, programs, logger, filepath, gzip }: GuideProps) { - this.channels = channels - this.programs = programs - this.logger = logger - this.storage = new Storage() - this.filepath = filepath - this.gzip = gzip || false - } - - async save() { - const channels = this.channels.uniqBy( - (channel: Channel) => `${channel.xmltv_id}:${channel.site}` - ) - const programs = this.programs - - const xmltv = new XMLTV({ - channels, - programs, - date: new DateTime(CURR_DATE, { zone: 'UTC' }) - }) - - const xmlFilepath = this.filepath - this.logger.info(` saving to "${xmlFilepath}"...`) - await this.storage.save(xmlFilepath, xmltv.toString()) - - if (this.gzip) { - const zip = new Zip() - const compressed = await zip.compress(xmltv.toString()) - const gzFilepath = `${this.filepath}.gz` - this.logger.info(` saving to "${gzFilepath}"...`) - await this.storage.save(gzFilepath, compressed) - } - } -} +import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core' +import { Channel } from 'epg-grabber' +import { XMLTV } from '../core' +import { CURR_DATE } from '../constants' + +type GuideProps = { + channels: Collection + programs: Collection + logger: Logger + filepath: string + gzip: boolean +} + +export class Guide { + channels: Collection + programs: Collection + logger: Logger + storage: Storage + filepath: string + gzip: boolean + + constructor({ channels, programs, logger, filepath, gzip }: GuideProps) { + this.channels = channels + this.programs = programs + this.logger = logger + this.storage = new Storage() + this.filepath = filepath + this.gzip = gzip || false + } + + async save() { + const channels = this.channels.uniqBy( + (channel: Channel) => `${channel.xmltv_id}:${channel.site}` + ) + const programs = this.programs + + const xmltv = new XMLTV({ + channels, + programs, + date: new DateTime(CURR_DATE, { zone: 'UTC' }) + }) + + const xmlFilepath = this.filepath + this.logger.info(` saving to "${xmlFilepath}"...`) + await this.storage.save(xmlFilepath, xmltv.toString()) + + if (this.gzip) { + const zip = new Zip() + const compressed = await zip.compress(xmltv.toString()) + const gzFilepath = `${this.filepath}.gz` + this.logger.info(` saving to "${gzFilepath}"...`) + await this.storage.save(gzFilepath, compressed) + } + } +} diff --git a/scripts/core/guideManager.ts b/scripts/core/guideManager.ts index 22b805522..78640a328 100644 --- a/scripts/core/guideManager.ts +++ b/scripts/core/guideManager.ts @@ -1,61 +1,61 @@ -import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core' -import { OptionValues } from 'commander' -import { Channel, Program } from 'epg-grabber' -import { Guide } from '.' - -type GuideManagerProps = { - options: OptionValues - logger: Logger - channels: Collection - programs: Collection -} - -export class GuideManager { - options: OptionValues - storage: Storage - logger: Logger - channels: Collection - programs: Collection - - constructor({ channels, programs, logger, options }: GuideManagerProps) { - this.options = options - this.logger = logger - this.channels = channels - this.programs = programs - this.storage = new Storage() - } - - async createGuides() { - const pathTemplate = new StringTemplate(this.options.output) - - const groupedChannels = this.channels - .orderBy([(channel: Channel) => channel.xmltv_id]) - .uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`) - .groupBy((channel: Channel) => { - return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' }) - }) - - const groupedPrograms = this.programs - .orderBy([(program: Program) => program.channel, (program: Program) => program.start]) - .groupBy((program: Program) => { - const lang = - program.titles && program.titles.length && program.titles[0].lang - ? program.titles[0].lang - : 'en' - - return pathTemplate.format({ lang, site: program.site || '' }) - }) - - for (const groupKey of groupedPrograms.keys()) { - const guide = new Guide({ - filepath: groupKey, - gzip: this.options.gzip, - channels: new Collection(groupedChannels.get(groupKey)), - programs: new Collection(groupedPrograms.get(groupKey)), - logger: this.logger - }) - - await guide.save() - } - } -} +import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core' +import { OptionValues } from 'commander' +import { Channel, Program } from 'epg-grabber' +import { Guide } from '.' + +type GuideManagerProps = { + options: OptionValues + logger: Logger + channels: Collection + programs: Collection +} + +export class GuideManager { + options: OptionValues + storage: Storage + logger: Logger + channels: Collection + programs: Collection + + constructor({ channels, programs, logger, options }: GuideManagerProps) { + this.options = options + this.logger = logger + this.channels = channels + this.programs = programs + this.storage = new Storage() + } + + async createGuides() { + const pathTemplate = new StringTemplate(this.options.output) + + const groupedChannels = this.channels + .orderBy([(channel: Channel) => channel.xmltv_id]) + .uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`) + .groupBy((channel: Channel) => { + return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' }) + }) + + const groupedPrograms = this.programs + .orderBy([(program: Program) => program.channel, (program: Program) => program.start]) + .groupBy((program: Program) => { + const lang = + program.titles && program.titles.length && program.titles[0].lang + ? program.titles[0].lang + : 'en' + + return pathTemplate.format({ lang, site: program.site || '' }) + }) + + for (const groupKey of groupedPrograms.keys()) { + const guide = new Guide({ + filepath: groupKey, + gzip: this.options.gzip, + channels: new Collection(groupedChannels.get(groupKey)), + programs: new Collection(groupedPrograms.get(groupKey)), + logger: this.logger + }) + + await guide.save() + } + } +} diff --git a/scripts/core/index.ts b/scripts/core/index.ts index 0af8e6365..fca249a5e 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -1,12 +1,12 @@ -export * from './xml' -export * from './channelsParser' -export * from './xmltv' -export * from './configLoader' -export * from './grabber' -export * from './job' -export * from './queue' -export * from './guideManager' -export * from './guide' -export * from './apiChannel' -export * from './apiClient' -export * from './queueCreator' +export * from './xml' +export * from './channelsParser' +export * from './xmltv' +export * from './configLoader' +export * from './grabber' +export * from './job' +export * from './queue' +export * from './guideManager' +export * from './guide' +export * from './apiChannel' +export * from './apiClient' +export * from './queueCreator' diff --git a/scripts/core/job.ts b/scripts/core/job.ts index d23b59f6e..9dbf7308a 100644 --- a/scripts/core/job.ts +++ b/scripts/core/job.ts @@ -1,34 +1,34 @@ -import { Logger } from '@freearhey/core' -import { Queue, Grabber, GuideManager } from '.' -import { GrabOptions } from '../commands/epg/grab' - -type JobProps = { - options: GrabOptions - logger: Logger - queue: Queue -} - -export class Job { - options: GrabOptions - logger: Logger - grabber: Grabber - - constructor({ queue, logger, options }: JobProps) { - this.options = options - this.logger = logger - this.grabber = new Grabber({ logger, queue, options }) - } - - async run() { - const { channels, programs } = await this.grabber.grab() - - const manager = new GuideManager({ - channels, - programs, - options: this.options, - logger: this.logger - }) - - await manager.createGuides() - } -} +import { Logger } from '@freearhey/core' +import { Queue, Grabber, GuideManager } from '.' +import { GrabOptions } from '../commands/epg/grab' + +type JobProps = { + options: GrabOptions + logger: Logger + queue: Queue +} + +export class Job { + options: GrabOptions + logger: Logger + grabber: Grabber + + constructor({ queue, logger, options }: JobProps) { + this.options = options + this.logger = logger + this.grabber = new Grabber({ logger, queue, options }) + } + + async run() { + const { channels, programs } = await this.grabber.grab() + + const manager = new GuideManager({ + channels, + programs, + options: this.options, + logger: this.logger + }) + + await manager.createGuides() + } +} diff --git a/scripts/core/queue.ts b/scripts/core/queue.ts index 28af9d881..06ef39616 100644 --- a/scripts/core/queue.ts +++ b/scripts/core/queue.ts @@ -1,45 +1,45 @@ -import { Dictionary } from '@freearhey/core' -import { SiteConfig, Channel } from 'epg-grabber' - -export type QueueItem = { - channel: Channel - date: string - config: SiteConfig - error: string | null -} - -export class Queue { - _data: Dictionary - - constructor() { - this._data = new Dictionary() - } - - missing(key: string): boolean { - return this._data.missing(key) - } - - add( - key: string, - { channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig } - ) { - this._data.set(key, { - channel, - date, - config, - error: null - }) - } - - size(): number { - return Object.values(this._data.data()).length - } - - items(): QueueItem[] { - return Object.values(this._data.data()) as QueueItem[] - } - - isEmpty(): boolean { - return this.size() === 0 - } -} +import { Dictionary } from '@freearhey/core' +import { SiteConfig, Channel } from 'epg-grabber' + +export type QueueItem = { + channel: Channel + date: string + config: SiteConfig + error: string | null +} + +export class Queue { + _data: Dictionary + + constructor() { + this._data = new Dictionary() + } + + missing(key: string): boolean { + return this._data.missing(key) + } + + add( + key: string, + { channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig } + ) { + this._data.set(key, { + channel, + date, + config, + error: null + }) + } + + size(): number { + return Object.values(this._data.data()).length + } + + items(): QueueItem[] { + return Object.values(this._data.data()) as QueueItem[] + } + + isEmpty(): boolean { + return this.size() === 0 + } +} diff --git a/scripts/core/queueCreator.ts b/scripts/core/queueCreator.ts index b23a8f4a8..ed0e54353 100644 --- a/scripts/core/queueCreator.ts +++ b/scripts/core/queueCreator.ts @@ -1,71 +1,71 @@ -import { Storage, Collection, DateTime, Logger } from '@freearhey/core' -import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './' -import { SITES_DIR, DATA_DIR, CURR_DATE } from '../constants' -import { SiteConfig } from 'epg-grabber' -import path from 'path' -import { GrabOptions } from '../commands/epg/grab' - -type QueueCreatorProps = { - logger: Logger - options: GrabOptions - parsedChannels: Collection -} - -export class QueueCreator { - configLoader: ConfigLoader - logger: Logger - sitesStorage: Storage - dataStorage: Storage - parser: ChannelsParser - parsedChannels: Collection - options: GrabOptions - date: DateTime - - constructor({ parsedChannels, logger, options }: QueueCreatorProps) { - this.parsedChannels = parsedChannels - this.logger = logger - this.sitesStorage = new Storage() - this.dataStorage = new Storage(DATA_DIR) - this.parser = new ChannelsParser({ storage: new Storage() }) - this.date = new DateTime(CURR_DATE) - this.options = options - this.configLoader = new ConfigLoader() - } - - async create(): Promise { - const channelsContent = await this.dataStorage.json('channels.json') - const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) - - const queue = new Queue() - for (const channel of this.parsedChannels.all()) { - if (!channel.site || !channel.xmltv_id) continue - if (this.options.lang && channel.lang !== this.options.lang) continue - - const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) - const config: SiteConfig = await this.configLoader.load(configPath) - - const found: ApiChannel = channels.first( - (_channel: ApiChannel) => _channel.id === channel.xmltv_id - ) - if (found) { - channel.logo = found.logo - } - - const days = this.options.days || config.days || 1 - const dates = Array.from({ length: days }, (_, day) => this.date.add(day, 'd')) - dates.forEach((date: DateTime) => { - const dateString = date.toJSON() - const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}` - if (queue.missing(key)) { - queue.add(key, { - channel, - date: dateString, - config - }) - } - }) - } - - return queue - } -} +import { Storage, Collection, DateTime, Logger } from '@freearhey/core' +import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './' +import { SITES_DIR, DATA_DIR, CURR_DATE } from '../constants' +import { SiteConfig } from 'epg-grabber' +import path from 'path' +import { GrabOptions } from '../commands/epg/grab' + +type QueueCreatorProps = { + logger: Logger + options: GrabOptions + parsedChannels: Collection +} + +export class QueueCreator { + configLoader: ConfigLoader + logger: Logger + sitesStorage: Storage + dataStorage: Storage + parser: ChannelsParser + parsedChannels: Collection + options: GrabOptions + date: DateTime + + constructor({ parsedChannels, logger, options }: QueueCreatorProps) { + this.parsedChannels = parsedChannels + this.logger = logger + this.sitesStorage = new Storage() + this.dataStorage = new Storage(DATA_DIR) + this.parser = new ChannelsParser({ storage: new Storage() }) + this.date = new DateTime(CURR_DATE) + this.options = options + this.configLoader = new ConfigLoader() + } + + async create(): Promise { + const channelsContent = await this.dataStorage.json('channels.json') + const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) + + const queue = new Queue() + for (const channel of this.parsedChannels.all()) { + if (!channel.site || !channel.xmltv_id) continue + if (this.options.lang && channel.lang !== this.options.lang) continue + + const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) + const config: SiteConfig = await this.configLoader.load(configPath) + + const found: ApiChannel = channels.first( + (_channel: ApiChannel) => _channel.id === channel.xmltv_id + ) + if (found) { + channel.logo = found.logo + } + + const days = this.options.days || config.days || 1 + const dates = Array.from({ length: days }, (_, day) => this.date.add(day, 'd')) + dates.forEach((date: DateTime) => { + const dateString = date.toJSON() + const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}` + if (queue.missing(key)) { + queue.add(key, { + channel, + date: dateString, + config + }) + } + }) + } + + return queue + } +} diff --git a/scripts/core/xml.ts b/scripts/core/xml.ts index 8c644fa28..fd42f9939 100644 --- a/scripts/core/xml.ts +++ b/scripts/core/xml.ts @@ -1,56 +1,56 @@ -import { Collection } from '@freearhey/core' -import { Channel } from 'epg-grabber' - -export class XML { - items: Collection - - constructor(items: Collection) { - this.items = items - } - - toString() { - let output = '\r\n\r\n' - - this.items.forEach((channel: Channel) => { - const logo = channel.logo ? ` logo="${channel.logo}"` : '' - const xmltv_id = channel.xmltv_id || '' - const lang = channel.lang || '' - const site_id = channel.site_id || '' - output += ` ${escapeString(channel.name)}\r\n` - }) - - output += '\r\n' - - return output - } -} - -function escapeString(value: string, defaultValue: string = '') { - if (!value) return defaultValue - - const regex = new RegExp( - '((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' + - 'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' + - 'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' + - '|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' + - 'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' + - '[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' + - 'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' + - '(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))', - 'g' - ) - - value = String(value || '').replace(regex, '') - - return value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\n|\r/g, ' ') - .replace(/ +/g, ' ') - .trim() -} +import { Collection } from '@freearhey/core' +import { Channel } from 'epg-grabber' + +export class XML { + items: Collection + + constructor(items: Collection) { + this.items = items + } + + toString() { + let output = '\r\n\r\n' + + this.items.forEach((channel: Channel) => { + const logo = channel.logo ? ` logo="${channel.logo}"` : '' + const xmltv_id = channel.xmltv_id || '' + const lang = channel.lang || '' + const site_id = channel.site_id || '' + output += ` ${escapeString(channel.name)}\r\n` + }) + + output += '\r\n' + + return output + } +} + +function escapeString(value: string, defaultValue: string = '') { + if (!value) return defaultValue + + const regex = new RegExp( + '((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' + + 'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' + + 'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' + + '|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' + + 'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' + + '[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' + + 'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' + + '(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))', + 'g' + ) + + value = String(value || '').replace(regex, '') + + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\n|\r/g, ' ') + .replace(/ +/g, ' ') + .trim() +} diff --git a/scripts/core/xmltv.ts b/scripts/core/xmltv.ts index eebd54240..5603c4e78 100644 --- a/scripts/core/xmltv.ts +++ b/scripts/core/xmltv.ts @@ -1,28 +1,28 @@ -import { DateTime, Collection } from '@freearhey/core' -import { generateXMLTV } from 'epg-grabber' - -type XMLTVProps = { - channels: Collection - programs: Collection - date: DateTime -} - -export class XMLTV { - channels: Collection - programs: Collection - date: DateTime - - constructor({ channels, programs, date }: XMLTVProps) { - this.channels = channels - this.programs = programs - this.date = date - } - - toString() { - return generateXMLTV({ - channels: this.channels.all(), - programs: this.programs.all(), - date: this.date.toJSON() - }) - } -} +import { DateTime, Collection } from '@freearhey/core' +import { generateXMLTV } from 'epg-grabber' + +type XMLTVProps = { + channels: Collection + programs: Collection + date: DateTime +} + +export class XMLTV { + channels: Collection + programs: Collection + date: DateTime + + constructor({ channels, programs, date }: XMLTVProps) { + this.channels = channels + this.programs = programs + this.date = date + } + + toString() { + return generateXMLTV({ + channels: this.channels.all(), + programs: this.programs.all(), + date: this.date.toJSON() + }) + } +} diff --git a/scripts/types/langs.d.ts b/scripts/types/langs.d.ts index 60fb498a7..74921c685 100644 --- a/scripts/types/langs.d.ts +++ b/scripts/types/langs.d.ts @@ -1 +1 @@ -declare module 'langs' +declare module 'langs' diff --git a/sites/9tv.co.il/9tv.co.il.config.js b/sites/9tv.co.il/9tv.co.il.config.js index fcc1c312b..aafd4bb61 100644 --- a/sites/9tv.co.il/9tv.co.il.config.js +++ b/sites/9tv.co.il/9tv.co.il.config.js @@ -1,69 +1,69 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: '9tv.co.il', - days: 2, - url: function ({ date }) { - return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format( - 'DD/MM/YYYY 00:00:00' - )}` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const start = parseStart($item, date) - if (prev) prev.stop = start - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - icon: parseIcon($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - let time = $item('a > div.guide_list_time').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem') -} - -function parseIcon($item) { - const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css( - 'background-image' - ) - if (!backgroundImage) return null - const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null] - - return relativePath ? `https://www.9tv.co.il${relativePath}` : null -} - -function parseDescription($item) { - return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim() -} - -function parseTitle($item) { - return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('li').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: '9tv.co.il', + days: 2, + url: function ({ date }) { + return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format( + 'DD/MM/YYYY 00:00:00' + )}` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const start = parseStart($item, date) + if (prev) prev.stop = start + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + icon: parseIcon($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + let time = $item('a > div.guide_list_time').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem') +} + +function parseIcon($item) { + const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css( + 'background-image' + ) + if (!backgroundImage) return null + const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null] + + return relativePath ? `https://www.9tv.co.il${relativePath}` : null +} + +function parseDescription($item) { + return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim() +} + +function parseTitle($item) { + return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('li').toArray() +} diff --git a/sites/9tv.co.il/9tv.co.il.test.js b/sites/9tv.co.il/9tv.co.il.test.js index fce629cad..90be4841d 100644 --- a/sites/9tv.co.il/9tv.co.il.test.js +++ b/sites/9tv.co.il/9tv.co.il.test.js @@ -1,57 +1,57 @@ -// npm run grab -- --site=9tv.co.il - -const { parser, url } = require('./9tv.co.il.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'Channel9.il' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00' - ) -}) - -it('can parse response', () => { - const content = - '
  • 06:30

    Слепая

    Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. 
  • 09:10

    Орел и решка. Морской сезон

    Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.
  • ' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-06T04:30:00.000Z', - stop: '2022-03-06T07:10:00.000Z', - title: 'Слепая', - icon: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg', - description: - 'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.' - }, - { - start: '2022-03-06T07:10:00.000Z', - stop: '2022-03-06T08:10:00.000Z', - icon: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg', - title: 'Орел и решка. Морской сезон', - description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=9tv.co.il + +const { parser, url } = require('./9tv.co.il.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'Channel9.il' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00' + ) +}) + +it('can parse response', () => { + const content = + '
  • 06:30

    Слепая

    Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. 
  • 09:10

    Орел и решка. Морской сезон

    Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.
  • ' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-06T04:30:00.000Z', + stop: '2022-03-06T07:10:00.000Z', + title: 'Слепая', + icon: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg', + description: + 'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.' + }, + { + start: '2022-03-06T07:10:00.000Z', + stop: '2022-03-06T08:10:00.000Z', + icon: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg', + title: 'Орел и решка. Морской сезон', + description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/abc.net.au/abc.net.au.config.js b/sites/abc.net.au/abc.net.au.config.js index b3212833c..26388f477 100644 --- a/sites/abc.net.au/abc.net.au.config.js +++ b/sites/abc.net.au/abc.net.au.config.js @@ -1,77 +1,77 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'abc.net.au', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://epg.abctv.net.au/processed/Sydney_${date.format('YYYY-MM-DD')}.json` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.episode_title, - category: item.genres, - description: item.description, - season: parseSeason(item), - episode: parseEpisode(item), - rating: parseRating(item), - icon: parseIcon(item), - start: parseTime(item.start_time), - stop: parseTime(item.end_time) - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!data) return [] - if (!Array.isArray(data.schedule)) return [] - - const channelData = data.schedule.find(i => i.channel == channel.site_id) - return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] - } catch (err) { - return [] - } -} - -function parseSeason(item) { - return item.series_num || null -} -function parseEpisode(item) { - return item.episode_num || null -} -function parseTime(time) { - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney') -} -function parseIcon(item) { - return item.image_file - ? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}` - : null -} -function parseRating(item) { - return item.rating - ? { - system: 'ACB', - value: item.rating - } - : null -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'abc.net.au', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://epg.abctv.net.au/processed/Sydney_${date.format('YYYY-MM-DD')}.json` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.episode_title, + category: item.genres, + description: item.description, + season: parseSeason(item), + episode: parseEpisode(item), + rating: parseRating(item), + icon: parseIcon(item), + start: parseTime(item.start_time), + stop: parseTime(item.end_time) + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!data) return [] + if (!Array.isArray(data.schedule)) return [] + + const channelData = data.schedule.find(i => i.channel == channel.site_id) + return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] + } catch (err) { + return [] + } +} + +function parseSeason(item) { + return item.series_num || null +} +function parseEpisode(item) { + return item.episode_num || null +} +function parseTime(time) { + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney') +} +function parseIcon(item) { + return item.image_file + ? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}` + : null +} +function parseRating(item) { + return item.rating + ? { + system: 'ACB', + value: item.rating + } + : null +} diff --git a/sites/abc.net.au/abc.net.au.test.js b/sites/abc.net.au/abc.net.au.test.js index 29e720c8a..791302a7a 100644 --- a/sites/abc.net.au/abc.net.au.test.js +++ b/sites/abc.net.au/abc.net.au.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=abc.net.au - -const { parser, url } = require('./abc.net.au.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ABC1', - xmltv_id: 'ABCTV.au' -} -it('can generate valid url', () => { - expect(url({ date })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json') -}) - -it('can parse response', () => { - const content = - '{"date":"2022-12-22","region":"Sydney","schedule":[{"channel":"ABC1","listing":[{"consumer_advice":"Adult Themes, Drug Use, Violence","rating":"M","show_id":912747,"repeat":true,"description":"When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?","title":"Silent Witness","crid":"ZW2178A004S00","start_time":"2022-12-22T00:46:00","series-crid":"ZW2178A","live":false,"captioning":true,"show_type":"Episode","series_num":22,"episode_title":"Lift Up Your Hearts (part Two)","length":58,"onair_title":"Silent Witness","end_time":"2022-12-22T01:44:00","genres":["Entertainment"],"image_file":"ZW2178A004S00_460.jpg","prog_slug":"silent-witness","episode_num":4}]}]}' - - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Silent Witness', - sub_title: 'Lift Up Your Hearts (part Two)', - description: - 'When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?', - category: ['Entertainment'], - rating: { - system: 'ACB', - value: 'M' - }, - season: 22, - episode: 4, - icon: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg', - start: '2022-12-21T13:46:00.000Z', - stop: '2022-12-21T14:44:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser( - { - content: - 'NoSuchKeyThe specified key does not exist.processed/Sydney_2023-01-17.json6MRHX5TJ12X39B3Y59rH6XRMrmkFywg8Kv58iqpI6O1fuOCuEbKa1HRRYa4buByXMBTvAhz8zuAK7X5D+ZN9ZuWxyGs=' - }, - channel - ) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=abc.net.au + +const { parser, url } = require('./abc.net.au.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ABC1', + xmltv_id: 'ABCTV.au' +} +it('can generate valid url', () => { + expect(url({ date })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json') +}) + +it('can parse response', () => { + const content = + '{"date":"2022-12-22","region":"Sydney","schedule":[{"channel":"ABC1","listing":[{"consumer_advice":"Adult Themes, Drug Use, Violence","rating":"M","show_id":912747,"repeat":true,"description":"When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?","title":"Silent Witness","crid":"ZW2178A004S00","start_time":"2022-12-22T00:46:00","series-crid":"ZW2178A","live":false,"captioning":true,"show_type":"Episode","series_num":22,"episode_title":"Lift Up Your Hearts (part Two)","length":58,"onair_title":"Silent Witness","end_time":"2022-12-22T01:44:00","genres":["Entertainment"],"image_file":"ZW2178A004S00_460.jpg","prog_slug":"silent-witness","episode_num":4}]}]}' + + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Silent Witness', + sub_title: 'Lift Up Your Hearts (part Two)', + description: + 'When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?', + category: ['Entertainment'], + rating: { + system: 'ACB', + value: 'M' + }, + season: 22, + episode: 4, + icon: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg', + start: '2022-12-21T13:46:00.000Z', + stop: '2022-12-21T14:44:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser( + { + content: + 'NoSuchKeyThe specified key does not exist.processed/Sydney_2023-01-17.json6MRHX5TJ12X39B3Y59rH6XRMrmkFywg8Kv58iqpI6O1fuOCuEbKa1HRRYa4buByXMBTvAhz8zuAK7X5D+ZN9ZuWxyGs=' + }, + channel + ) + expect(result).toMatchObject([]) +}) diff --git a/sites/allente.se/allente.se.config.js b/sites/allente.se/allente.se.config.js index f941be0b9..37d5bf364 100644 --- a/sites/allente.se/allente.se.config.js +++ b/sites/allente.se/allente.se.config.js @@ -1,63 +1,63 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'allente.se', - days: 2, - url({ date, channel }) { - const [country] = channel.site_id.split('#') - - return `https://cs-vcb.allente.${country}/epg/events?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (!item.details) return - const start = dayjs(item.time) - const stop = start.add(item.details.duration, 'm') - programs.push({ - title: item.title, - category: item.details.categories, - description: item.details.description, - icon: item.details.image, - season: parseSeason(item), - episode: parseEpisode(item), - start, - stop - }) - }) - - return programs - }, - async channels({ country, lang }) { - const data = await axios - .get(`https://cs-vcb.allente.${country}/epg/events?date=2021-11-17`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang, - site_id: `${country}#${item.id}`, - name: item.name - } - }) - } -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(i => i.id === channelId) - - return channelData && Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(item) { - return item.details.season || null -} -function parseEpisode(item) { - return item.details.episode || null -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'allente.se', + days: 2, + url({ date, channel }) { + const [country] = channel.site_id.split('#') + + return `https://cs-vcb.allente.${country}/epg/events?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (!item.details) return + const start = dayjs(item.time) + const stop = start.add(item.details.duration, 'm') + programs.push({ + title: item.title, + category: item.details.categories, + description: item.details.description, + icon: item.details.image, + season: parseSeason(item), + episode: parseEpisode(item), + start, + stop + }) + }) + + return programs + }, + async channels({ country, lang }) { + const data = await axios + .get(`https://cs-vcb.allente.${country}/epg/events?date=2021-11-17`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang, + site_id: `${country}#${item.id}`, + name: item.name + } + }) + } +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(i => i.id === channelId) + + return channelData && Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(item) { + return item.details.season || null +} +function parseEpisode(item) { + return item.details.episode || null +} diff --git a/sites/allente.se/allente.se.test.js b/sites/allente.se/allente.se.test.js index 6492b5e72..f869aff48 100644 --- a/sites/allente.se/allente.se.test.js +++ b/sites/allente.se/allente.se.test.js @@ -1,62 +1,62 @@ -// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_se.channels.xml --set=country:se --set=lang:sv -// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_fi.channels.xml --set=country:fi --set=lang:fi -// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_no.channels.xml --set=country:no --set=lang:no -// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_dk.channels.xml --set=country:dk --set=lang:da -// npm run grab -- --site=allente.se - -const { parser, url } = require('./allente.se.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'se#0148', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17') -}) - -it('can generate valid url for different country', () => { - const dkChannel = { site_id: 'dk#0148' } - expect(url({ date, channel: dkChannel })).toBe( - 'https://cs-vcb.allente.dk/epg/events?date=2021-11-17' - ) -}) - -it('can parse response', () => { - const content = - '{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-22T07:10:00.000Z', - stop: '2022-08-22T07:30:00.000Z', - title: 'Hemmagympa med Sofia', - category: ['other'], - description: - 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', - icon: 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', - season: 4, - episode: 1 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"date":"2001-11-17","categories":[],"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_se.channels.xml --set=country:se --set=lang:sv +// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_fi.channels.xml --set=country:fi --set=lang:fi +// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_no.channels.xml --set=country:no --set=lang:no +// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_dk.channels.xml --set=country:dk --set=lang:da +// npm run grab -- --site=allente.se + +const { parser, url } = require('./allente.se.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'se#0148', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17') +}) + +it('can generate valid url for different country', () => { + const dkChannel = { site_id: 'dk#0148' } + expect(url({ date, channel: dkChannel })).toBe( + 'https://cs-vcb.allente.dk/epg/events?date=2021-11-17' + ) +}) + +it('can parse response', () => { + const content = + '{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-22T07:10:00.000Z', + stop: '2022-08-22T07:30:00.000Z', + title: 'Hemmagympa med Sofia', + category: ['other'], + description: + 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', + icon: 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', + season: 4, + episode: 1 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"date":"2001-11-17","categories":[],"channels":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/andorradifusio.ad/andorradifusio.ad.config.js b/sites/andorradifusio.ad/andorradifusio.ad.config.js index c20b29001..cf3e5a5de 100644 --- a/sites/andorradifusio.ad/andorradifusio.ad.config.js +++ b/sites/andorradifusio.ad/andorradifusio.ad.config.js @@ -1,59 +1,59 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'andorradifusio.ad', - days: 2, - url({ channel }) { - return `https://www.andorradifusio.ad/programacio/${channel.site_id}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('MM/DD/YYYY')} ${item.time}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase() - const column = $('.programacio-dia > h3 > .dia') - .filter((i, el) => $(el).text() === day.slice(0, 6) + '.') - .first() - .parent() - .parent() - const items = [] - const titles = column.find('p').toArray() - column.find('h4').each((i, time) => { - items.push({ - time: $(time).text(), - title: $(titles[i]).text() - }) - }) - - return items -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'andorradifusio.ad', + days: 2, + url({ channel }) { + return `https://www.andorradifusio.ad/programacio/${channel.site_id}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('MM/DD/YYYY')} ${item.time}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase() + const column = $('.programacio-dia > h3 > .dia') + .filter((i, el) => $(el).text() === day.slice(0, 6) + '.') + .first() + .parent() + .parent() + const items = [] + const titles = column.find('p').toArray() + column.find('h4').each((i, time) => { + items.push({ + time: $(time).text(), + title: $(titles[i]).text() + }) + }) + + return items +} diff --git a/sites/andorradifusio.ad/andorradifusio.ad.test.js b/sites/andorradifusio.ad/andorradifusio.ad.test.js index 1e54024bf..a89961389 100644 --- a/sites/andorradifusio.ad/andorradifusio.ad.test.js +++ b/sites/andorradifusio.ad/andorradifusio.ad.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=andorradifusio.ad - -const { parser, url } = require('./andorradifusio.ad.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'atv', - xmltv_id: 'AndorraTV.ad' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-07T05:00:00.000Z', - stop: '2023-06-07T06:00:00.000Z', - title: 'Club Piolet' - }) - - expect(results[20]).toMatchObject({ - start: '2023-06-07T23:00:00.000Z', - stop: '2023-06-08T00:00:00.000Z', - title: 'Àrea Andorra Difusió' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=andorradifusio.ad + +const { parser, url } = require('./andorradifusio.ad.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'atv', + xmltv_id: 'AndorraTV.ad' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-07T05:00:00.000Z', + stop: '2023-06-07T06:00:00.000Z', + title: 'Club Piolet' + }) + + expect(results[20]).toMatchObject({ + start: '2023-06-07T23:00:00.000Z', + stop: '2023-06-08T00:00:00.000Z', + title: 'Àrea Andorra Difusió' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/arianaafgtv.com/arianaafgtv.com.config.js b/sites/arianaafgtv.com/arianaafgtv.com.config.js index 69a20807f..17e7011c1 100644 --- a/sites/arianaafgtv.com/arianaafgtv.com.config.js +++ b/sites/arianaafgtv.com/arianaafgtv.com.config.js @@ -1,82 +1,82 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'arianaafgtv.com', - days: 2, - url: 'https://www.arianaafgtv.com/index.html', - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const title = item.title - const start = parseStart(item, date) - const stop = parseStop(item, date) - programs.push({ - title, - start, - stop - }) - }) - - return programs - } -} - -function parseStop(item, date) { - const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}` - - return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') -} - -function parseStart(item, date) { - const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}` - - return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const dayOfWeek = date.format('dddd') - const column = $('.H4') - .filter((i, el) => { - return $(el).text() === dayOfWeek - }) - .first() - .parent() - - const rows = column - .find('.Paragraph') - .map((i, el) => { - return $(el).html() - }) - .toArray() - .map(r => (r === ' ' ? '|' : r)) - .join(' ') - .split('|') - - const items = [] - rows.forEach(row => { - row = row.trim() - if (row) { - const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi) - if (!found) return - const time = found[0] - let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1] - start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') - let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1] - end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') - const title = row.replace(time, '').replace(' ', '').trim() - items.push({ start, end, title }) - } - }) - - return items -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'arianaafgtv.com', + days: 2, + url: 'https://www.arianaafgtv.com/index.html', + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const title = item.title + const start = parseStart(item, date) + const stop = parseStop(item, date) + programs.push({ + title, + start, + stop + }) + }) + + return programs + } +} + +function parseStop(item, date) { + const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}` + + return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') +} + +function parseStart(item, date) { + const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}` + + return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const dayOfWeek = date.format('dddd') + const column = $('.H4') + .filter((i, el) => { + return $(el).text() === dayOfWeek + }) + .first() + .parent() + + const rows = column + .find('.Paragraph') + .map((i, el) => { + return $(el).html() + }) + .toArray() + .map(r => (r === ' ' ? '|' : r)) + .join(' ') + .split('|') + + const items = [] + rows.forEach(row => { + row = row.trim() + if (row) { + const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi) + if (!found) return + const time = found[0] + let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1] + start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') + let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1] + end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') + const title = row.replace(time, '').replace(' ', '').trim() + items.push({ start, end, title }) + } + }) + + return items +} diff --git a/sites/arianatelevision.com/arianatelevision.com.config.js b/sites/arianatelevision.com/arianatelevision.com.config.js index 40bc9b798..d6284e46f 100644 --- a/sites/arianatelevision.com/arianatelevision.com.config.js +++ b/sites/arianatelevision.com/arianatelevision.com.config.js @@ -1,60 +1,60 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'arianatelevision.com', - days: 2, - url: 'https://www.arianatelevision.com/program-schedule/', - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.start}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const settings = $('#jtrt_table_settings_508').text() - if (!settings) return [] - const data = JSON.parse(settings) - if (!data || !Array.isArray(data)) return [] - - let rows = data[0] - rows.shift() - const output = [] - rows.forEach(row => { - let day = date.day() + 2 - if (day > 7) day = 1 - if (!row[0] || !row[day]) return - output.push({ - start: row[0].trim(), - title: row[day].trim() - }) - }) - - return output -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'arianatelevision.com', + days: 2, + url: 'https://www.arianatelevision.com/program-schedule/', + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.start}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const settings = $('#jtrt_table_settings_508').text() + if (!settings) return [] + const data = JSON.parse(settings) + if (!data || !Array.isArray(data)) return [] + + let rows = data[0] + rows.shift() + const output = [] + rows.forEach(row => { + let day = date.day() + 2 + if (day > 7) day = 1 + if (!row[0] || !row[day]) return + output.push({ + start: row[0].trim(), + title: row[day].trim() + }) + }) + + return output +} diff --git a/sites/arianatelevision.com/arianatelevision.com.test.js b/sites/arianatelevision.com/arianatelevision.com.test.js index d6ff37221..75f1e1cd1 100644 --- a/sites/arianatelevision.com/arianatelevision.com.test.js +++ b/sites/arianatelevision.com/arianatelevision.com.test.js @@ -1,61 +1,61 @@ -// npm run grab -- --site=arianatelevision.com - -const { parser, url } = require('./arianatelevision.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'ArianaTVNational.af' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.arianatelevision.com/program-schedule/') -}) - -it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-27T02:30:00.000Z', - stop: '2021-11-27T03:00:00.000Z', - title: 'City Report' - }, - { - start: '2021-11-27T03:00:00.000Z', - stop: '2021-11-27T10:30:00.000Z', - title: 'ICC T20 Highlights' - }, - { - start: '2021-11-27T10:30:00.000Z', - stop: '2021-11-28T02:00:00.000Z', - title: 'ICC T20 World Cup' - }, - { - start: '2021-11-28T02:00:00.000Z', - stop: '2021-11-28T02:30:00.000Z', - title: 'Quran and Hadis' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=arianatelevision.com + +const { parser, url } = require('./arianatelevision.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'ArianaTVNational.af' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.arianatelevision.com/program-schedule/') +}) + +it('can parse response', () => { + const content = + '' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-27T02:30:00.000Z', + stop: '2021-11-27T03:00:00.000Z', + title: 'City Report' + }, + { + start: '2021-11-27T03:00:00.000Z', + stop: '2021-11-27T10:30:00.000Z', + title: 'ICC T20 Highlights' + }, + { + start: '2021-11-27T10:30:00.000Z', + stop: '2021-11-28T02:00:00.000Z', + title: 'ICC T20 World Cup' + }, + { + start: '2021-11-28T02:00:00.000Z', + stop: '2021-11-28T02:30:00.000Z', + title: 'Quran and Hadis' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/arirang.com/arirang.com.config.js b/sites/arirang.com/arirang.com.config.js index 2ee402c73..17e9b911f 100644 --- a/sites/arirang.com/arirang.com.config.js +++ b/sites/arirang.com/arirang.com.config.js @@ -1,153 +1,153 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'arirang.com', - output: 'arirang.com.guide.xml', - channels: 'arirang.com.channels.xml', - lang: 'en', - days: 7, - delay: 5000, - url: 'https://www.arirang.com/v1.0/open/external/proxy', - - request: { - method: 'POST', - timeout: 5000, - cache: { ttl: 60 * 60 * 1000 }, - headers: { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - Origin: 'https://www.arirang.com', - Referer: 'https://www.arirang.com/schedule', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - }, - data: function (context) { - const { channel, date } = context - return { - address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', - method: 'POST', - headers: {}, - body: { - data: { - dmParam: { - chanId: channel.site_id, - broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'), - planNo: '1' - } - } - } - } - } - }, - - logo: function (context) { - return context.channel.logo - }, - - async parser(context) { - const programs = [] - const items = parseItems(context.content) - - for (let item of items) { - const programDetail = await parseProgramDetail(item) - - programs.push({ - title: item.displayNm, - start: parseStart(item), - stop: parseStop(item), - icon: parseIcon(programDetail), - category: parseCategory(programDetail), - description: parseDescription(programDetail) - }) - } - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek) - ? [] - : data.responseBody.dsSchWeek - } else { - return [] - } -} - -function parseStart(item) { - return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') -} - -function parseStop(item) { - return dayjs - .tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') - .add(item.broadRun, 'minute') -} - -async function parseProgramDetail(item) { - return axios - .post( - 'https://www.arirang.com/v1.0/open/program/detail', - { - bis_program_code: item.pgmCd - }, - { - headers: { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - Origin: 'https://www.arirang.com', - Referer: 'https://www.arirang.com/schedule', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - }, - timeout: 5000, - cache: { ttl: 60 * 1000 } - } - ) - .then(response => { - return response.data - }) - .catch(error => { - console.log(error) - }) -} - -function parseIcon(programDetail) { - if (programDetail && programDetail.image && programDetail.image[0].url) { - return programDetail.image[0].url - } else { - return '' - } -} - -function parseCategory(programDetail) { - if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) { - return programDetail.category_Info[0].title - } else { - return '' - } -} - -function parseDescription(programDetail) { - if ( - programDetail && - programDetail.content && - programDetail.content[0] && - programDetail.content[0].text - ) { - let description = programDetail.content[0].text - let regex = /(<([^>]+)>)/gi - return description.replace(regex, '') - } else { - return '' - } -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'arirang.com', + output: 'arirang.com.guide.xml', + channels: 'arirang.com.channels.xml', + lang: 'en', + days: 7, + delay: 5000, + url: 'https://www.arirang.com/v1.0/open/external/proxy', + + request: { + method: 'POST', + timeout: 5000, + cache: { ttl: 60 * 60 * 1000 }, + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + Origin: 'https://www.arirang.com', + Referer: 'https://www.arirang.com/schedule', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + }, + data: function (context) { + const { channel, date } = context + return { + address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', + method: 'POST', + headers: {}, + body: { + data: { + dmParam: { + chanId: channel.site_id, + broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'), + planNo: '1' + } + } + } + } + } + }, + + logo: function (context) { + return context.channel.logo + }, + + async parser(context) { + const programs = [] + const items = parseItems(context.content) + + for (let item of items) { + const programDetail = await parseProgramDetail(item) + + programs.push({ + title: item.displayNm, + start: parseStart(item), + stop: parseStop(item), + icon: parseIcon(programDetail), + category: parseCategory(programDetail), + description: parseDescription(programDetail) + }) + } + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek) + ? [] + : data.responseBody.dsSchWeek + } else { + return [] + } +} + +function parseStart(item) { + return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') +} + +function parseStop(item) { + return dayjs + .tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') + .add(item.broadRun, 'minute') +} + +async function parseProgramDetail(item) { + return axios + .post( + 'https://www.arirang.com/v1.0/open/program/detail', + { + bis_program_code: item.pgmCd + }, + { + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + Origin: 'https://www.arirang.com', + Referer: 'https://www.arirang.com/schedule', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + }, + timeout: 5000, + cache: { ttl: 60 * 1000 } + } + ) + .then(response => { + return response.data + }) + .catch(error => { + console.log(error) + }) +} + +function parseIcon(programDetail) { + if (programDetail && programDetail.image && programDetail.image[0].url) { + return programDetail.image[0].url + } else { + return '' + } +} + +function parseCategory(programDetail) { + if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) { + return programDetail.category_Info[0].title + } else { + return '' + } +} + +function parseDescription(programDetail) { + if ( + programDetail && + programDetail.content && + programDetail.content[0] && + programDetail.content[0].text + ) { + let description = programDetail.content[0].text + let regex = /(<([^>]+)>)/gi + return description.replace(regex, '') + } else { + return '' + } +} diff --git a/sites/arirang.com/arirang.com.test.js b/sites/arirang.com/arirang.com.test.js index 31d1b61bd..b0fd01255 100644 --- a/sites/arirang.com/arirang.com.test.js +++ b/sites/arirang.com/arirang.com.test.js @@ -1,74 +1,74 @@ -// npm run grab -- --site=arirang.com -// npx jest arirang.com.test.js - -const { url, parser } = require('./arirang.com.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.tz('2023-08-25', 'Asia/Seoul').startOf('d') -const channel = { - xmltv_id: 'ArirangWorld.kr', - site_id: 'CH_W', - name: 'Arirang World', - lang: 'en', - logo: 'https://i.imgur.com/5Aoithj.png' -} -const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8') -const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8') -const context = { channel: channel, content: content, date: date } - -it('can generate valid url', () => { - expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy') -}) - -it('can handle empty guide', async () => { - const results = await parser({ channel: channel, content: '', date: date }) - expect(results).toMatchObject([]) -}) - -it('can parse response', async () => { - axios.post.mockImplementation((url, data) => { - if ( - url === 'https://www.arirang.com/v1.0/open/external/proxy' && - JSON.stringify(data) === - JSON.stringify({ - address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', - method: 'POST', - headers: {}, - body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20230825', planNo: '1' } } } - }) - ) { - return Promise.resolve({ - data: JSON.parse(content) - }) - } else if ( - url === 'https://www.arirang.com/v1.0/open/program/detail' && - JSON.stringify(data) === JSON.stringify({ bis_program_code: '2023004T' }) - ) { - return Promise.resolve({ - data: JSON.parse(programDetail) - }) - } else { - return Promise.resolve({ - data: '' - }) - } - }) - - const results = await parser(context) - - expect(results[0]).toMatchObject({ - title: 'WITHIN THE FRAME [R]', - start: dayjs.tz(date, 'Asia/Seoul'), - stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'), - icon: 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202308/2080840096998752900.png', - description: 'NEWS', - category: 'Current Affairs' - }) -}) +// npm run grab -- --site=arirang.com +// npx jest arirang.com.test.js + +const { url, parser } = require('./arirang.com.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.tz('2023-08-25', 'Asia/Seoul').startOf('d') +const channel = { + xmltv_id: 'ArirangWorld.kr', + site_id: 'CH_W', + name: 'Arirang World', + lang: 'en', + logo: 'https://i.imgur.com/5Aoithj.png' +} +const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8') +const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8') +const context = { channel: channel, content: content, date: date } + +it('can generate valid url', () => { + expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy') +}) + +it('can handle empty guide', async () => { + const results = await parser({ channel: channel, content: '', date: date }) + expect(results).toMatchObject([]) +}) + +it('can parse response', async () => { + axios.post.mockImplementation((url, data) => { + if ( + url === 'https://www.arirang.com/v1.0/open/external/proxy' && + JSON.stringify(data) === + JSON.stringify({ + address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', + method: 'POST', + headers: {}, + body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20230825', planNo: '1' } } } + }) + ) { + return Promise.resolve({ + data: JSON.parse(content) + }) + } else if ( + url === 'https://www.arirang.com/v1.0/open/program/detail' && + JSON.stringify(data) === JSON.stringify({ bis_program_code: '2023004T' }) + ) { + return Promise.resolve({ + data: JSON.parse(programDetail) + }) + } else { + return Promise.resolve({ + data: '' + }) + } + }) + + const results = await parser(context) + + expect(results[0]).toMatchObject({ + title: 'WITHIN THE FRAME [R]', + start: dayjs.tz(date, 'Asia/Seoul'), + stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'), + icon: 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202308/2080840096998752900.png', + description: 'NEWS', + category: 'Current Affairs' + }) +}) diff --git a/sites/artonline.tv/artonline.tv.config.js b/sites/artonline.tv/artonline.tv.config.js index 54d5652a8..f00e6fd69 100644 --- a/sites/artonline.tv/artonline.tv.config.js +++ b/sites/artonline.tv/artonline.tv.config.js @@ -1,68 +1,68 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 - -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') -const dayjs = require('dayjs') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'artonline.tv', - days: 2, - url: function ({ channel }) { - return `https://www.artonline.tv/Home/Tvlist${channel.site_id}` - }, - request: { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data: function ({ date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - const params = new URLSearchParams() - params.append('objId', diff) - - return params - } - }, - parser: function ({ content }) { - const programs = [] - if (!content) return programs - const items = JSON.parse(content) - items.forEach(item => { - const icon = parseIcon(item) - const start = parseStart(item) - const duration = parseDuration(item) - const stop = start.add(duration, 's') - programs.push({ - title: item.title, - description: item.description, - icon, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /) - const [HH, mm] = item.start_Time.split(':') - - return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh') -} - -function parseDuration(item) { - const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/) - - return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss) -} - -function parseIcon(item) { - return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 + +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') +const dayjs = require('dayjs') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'artonline.tv', + days: 2, + url: function ({ channel }) { + return `https://www.artonline.tv/Home/Tvlist${channel.site_id}` + }, + request: { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + data: function ({ date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + const params = new URLSearchParams() + params.append('objId', diff) + + return params + } + }, + parser: function ({ content }) { + const programs = [] + if (!content) return programs + const items = JSON.parse(content) + items.forEach(item => { + const icon = parseIcon(item) + const start = parseStart(item) + const duration = parseDuration(item) + const stop = start.add(duration, 's') + programs.push({ + title: item.title, + description: item.description, + icon, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /) + const [HH, mm] = item.start_Time.split(':') + + return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh') +} + +function parseDuration(item) { + const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/) + + return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss) +} + +function parseIcon(item) { + return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null +} diff --git a/sites/artonline.tv/artonline.tv.test.js b/sites/artonline.tv/artonline.tv.test.js index 17ab430c1..43e52e899 100644 --- a/sites/artonline.tv/artonline.tv.test.js +++ b/sites/artonline.tv/artonline.tv.test.js @@ -1,67 +1,67 @@ -// npm run grab -- --site=artonline.tv - -const { parser, url, request } = require('./artonline.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const channel = { - site_id: 'Aflam2', - xmltv_id: 'ARTAflam2.sa' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'content-type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data for today', () => { - const date = dayjs.utc().startOf('d') - const data = request.data({ date }) - expect(data.get('objId')).toBe('0') -}) - -it('can generate valid request data for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - const data = request.data({ date }) - expect(data.get('objId')).toBe('1') -}) - -it('can parse response', () => { - const content = - '[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-03T21:30:00.000Z', - stop: '2022-03-03T23:04:00.000Z', - title: 'الراقصه و السياسي', - description: - 'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .', - icon: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=artonline.tv + +const { parser, url, request } = require('./artonline.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const channel = { + site_id: 'Aflam2', + xmltv_id: 'ARTAflam2.sa' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'content-type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data for today', () => { + const date = dayjs.utc().startOf('d') + const data = request.data({ date }) + expect(data.get('objId')).toBe('0') +}) + +it('can generate valid request data for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + const data = request.data({ date }) + expect(data.get('objId')).toBe('1') +}) + +it('can parse response', () => { + const content = + '[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-03T21:30:00.000Z', + stop: '2022-03-03T23:04:00.000Z', + title: 'الراقصه و السياسي', + description: + 'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .', + icon: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/astro.com.my/astro.com.my.config.js b/sites/astro.com.my/astro.com.my.config.js index 2711880a3..0680838cf 100644 --- a/sites/astro.com.my/astro.com.my.config.js +++ b/sites/astro.com.my/astro.com.my.config.js @@ -1,123 +1,123 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -const API_ENDPOINT = 'https://contenthub-api.eco.astro.com.my' - -module.exports = { - site: 'astro.com.my', - days: 2, - url: function ({ channel }) { - return `${API_ENDPOINT}/channel/${channel.site_id}.json` - }, - async parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - for (let item of items) { - const start = dayjs.utc(item.datetimeInUtc) - const duration = parseDuration(item.duration) - const stop = start.add(duration, 's') - const details = await loadProgramDetails(item) - programs.push({ - title: details.title, - sub_title: item.subtitles, - description: details.longSynopsis || details.shortSynopsis, - actors: parseList(details.cast), - directors: parseList(details.director), - icon: details.imageUrl, - rating: parseRating(details), - categories: parseCategories(details), - episode: parseEpisode(item), - season: parseSeason(details), - start: start, - stop: stop - }) - } - - return programs - } -} - -function parseEpisode(item) { - const [, number] = item.title.match(/Ep(\d+)$/) || [null, null] - - return number ? parseInt(number) : null -} - -function parseSeason(details) { - const [, season] = details.title ? details.title.match(/ S(\d+)/) || [null, null] : [null, null] - - return season ? parseInt(season) : null -} - -function parseList(list) { - return typeof list === 'string' ? list.split(',') : [] -} - -function parseRating(details) { - return details.certification - ? { - system: 'LPF', - value: details.certification - } - : null -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - const schedules = data.response.schedule - - return schedules[date.format('YYYY-MM-DD')] || [] - } catch (e) { - return [] - } -} - -function parseDuration(duration) { - const match = duration.match(/(\d{2}):(\d{2}):(\d{2})/) - const hours = parseInt(match[1]) - const minutes = parseInt(match[2]) - const seconds = parseInt(match[3]) - - return hours * 3600 + minutes * 60 + seconds -} - -function parseCategories(details) { - const genres = { - 'filter/2': 'Action', - 'filter/4': 'Anime', - 'filter/12': 'Cartoons', - 'filter/16': 'Comedy', - 'filter/19': 'Crime', - 'filter/24': 'Drama', - 'filter/25': 'Educational', - 'filter/36': 'Horror', - 'filter/39': 'Live Action', - 'filter/55': 'Pre-school', - 'filter/56': 'Reality', - 'filter/60': 'Romance', - 'filter/68': 'Talk Show', - 'filter/69': 'Thriller', - 'filter/72': 'Variety', - 'filter/75': 'Series', - 'filter/100': 'Others (Children)' - } - - return Array.isArray(details.subFilter) - ? details.subFilter.map(g => genres[g.toLowerCase()]).filter(Boolean) - : [] -} - -async function loadProgramDetails(item) { - const url = `${API_ENDPOINT}/api/v1/linear-detail?siTrafficKey=${item.siTrafficKey}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(error => console.log(error.message)) - if (!data) return {} - - return data.response || {} -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +const API_ENDPOINT = 'https://contenthub-api.eco.astro.com.my' + +module.exports = { + site: 'astro.com.my', + days: 2, + url: function ({ channel }) { + return `${API_ENDPOINT}/channel/${channel.site_id}.json` + }, + async parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + for (let item of items) { + const start = dayjs.utc(item.datetimeInUtc) + const duration = parseDuration(item.duration) + const stop = start.add(duration, 's') + const details = await loadProgramDetails(item) + programs.push({ + title: details.title, + sub_title: item.subtitles, + description: details.longSynopsis || details.shortSynopsis, + actors: parseList(details.cast), + directors: parseList(details.director), + icon: details.imageUrl, + rating: parseRating(details), + categories: parseCategories(details), + episode: parseEpisode(item), + season: parseSeason(details), + start: start, + stop: stop + }) + } + + return programs + } +} + +function parseEpisode(item) { + const [, number] = item.title.match(/Ep(\d+)$/) || [null, null] + + return number ? parseInt(number) : null +} + +function parseSeason(details) { + const [, season] = details.title ? details.title.match(/ S(\d+)/) || [null, null] : [null, null] + + return season ? parseInt(season) : null +} + +function parseList(list) { + return typeof list === 'string' ? list.split(',') : [] +} + +function parseRating(details) { + return details.certification + ? { + system: 'LPF', + value: details.certification + } + : null +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + const schedules = data.response.schedule + + return schedules[date.format('YYYY-MM-DD')] || [] + } catch (e) { + return [] + } +} + +function parseDuration(duration) { + const match = duration.match(/(\d{2}):(\d{2}):(\d{2})/) + const hours = parseInt(match[1]) + const minutes = parseInt(match[2]) + const seconds = parseInt(match[3]) + + return hours * 3600 + minutes * 60 + seconds +} + +function parseCategories(details) { + const genres = { + 'filter/2': 'Action', + 'filter/4': 'Anime', + 'filter/12': 'Cartoons', + 'filter/16': 'Comedy', + 'filter/19': 'Crime', + 'filter/24': 'Drama', + 'filter/25': 'Educational', + 'filter/36': 'Horror', + 'filter/39': 'Live Action', + 'filter/55': 'Pre-school', + 'filter/56': 'Reality', + 'filter/60': 'Romance', + 'filter/68': 'Talk Show', + 'filter/69': 'Thriller', + 'filter/72': 'Variety', + 'filter/75': 'Series', + 'filter/100': 'Others (Children)' + } + + return Array.isArray(details.subFilter) + ? details.subFilter.map(g => genres[g.toLowerCase()]).filter(Boolean) + : [] +} + +async function loadProgramDetails(item) { + const url = `${API_ENDPOINT}/api/v1/linear-detail?siTrafficKey=${item.siTrafficKey}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(error => console.log(error.message)) + if (!data) return {} + + return data.response || {} +} diff --git a/sites/astro.com.my/astro.com.my.test.js b/sites/astro.com.my/astro.com.my.test.js index 5c615f51d..755ec3d87 100644 --- a/sites/astro.com.my/astro.com.my.test.js +++ b/sites/astro.com.my/astro.com.my.test.js @@ -1,73 +1,73 @@ -// npm run grab -- --site=astro.com.my - -const { parser, url } = require('./astro.com.my.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-31', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '425', - xmltv_id: 'TVBClassic.hk' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(31) - expect(results[0]).toMatchObject({ - start: '2022-10-30T16:10:00.000Z', - stop: '2022-10-30T17:02:00.000Z', - title: 'Triumph in the Skies S1 Ep06', - description: - 'This classic drama depicts the many aspects of two complicated relationships set against an airline company. Will those involved ever find true love?', - actors: ['Francis Ng Chun Yu', 'Joe Ma Tak Chung', 'Flora Chan Wai San'], - directors: ['Joe Ma Tak Chung'], - icon: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg', - rating: { - system: 'LPF', - value: 'U' - }, - episode: 6, - season: 1, - categories: ['Drama'] - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const results = await parser({ date, content }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=astro.com.my + +const { parser, url } = require('./astro.com.my.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-31', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '425', + xmltv_id: 'TVBClassic.hk' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(31) + expect(results[0]).toMatchObject({ + start: '2022-10-30T16:10:00.000Z', + stop: '2022-10-30T17:02:00.000Z', + title: 'Triumph in the Skies S1 Ep06', + description: + 'This classic drama depicts the many aspects of two complicated relationships set against an airline company. Will those involved ever find true love?', + actors: ['Francis Ng Chun Yu', 'Joe Ma Tak Chung', 'Flora Chan Wai San'], + directors: ['Joe Ma Tak Chung'], + icon: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg', + rating: { + system: 'LPF', + value: 'U' + }, + episode: 6, + season: 1, + categories: ['Drama'] + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const results = await parser({ date, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/bein.com/bein.com.config.js b/sites/bein.com/bein.com.config.js index 8c1552ec9..898ea225c 100644 --- a/sites/bein.com/bein.com.config.js +++ b/sites/bein.com/bein.com.config.js @@ -1,79 +1,79 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'bein.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - const [category] = channel.site_id.split('#') - const postid = channel.lang === 'ar' ? '25344' : '25356' - - return `https://www.bein.com/${ - channel.lang - }/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format( - 'YYYY-MM-DD' - )}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 }) - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - if (!title) return - const category = parseCategory($item) - const prev = programs[programs.length - 1] - let start = parseTime($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.plus({ days: 1 }) - } - prev.stop = start - } - let stop = parseTime($item, start) - if (stop < start) { - stop = stop.plus({ days: 1 }) - } - programs.push({ - title, - category, - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.title').text() -} - -function parseCategory($item) { - return $item('.format').text() -} - -function parseTime($item, date) { - let [, time] = $item('.time') - .text() - .match(/^(\d{2}:\d{2})/) || [null, null] - if (!time) return null - time = `${date.toFormat('yyyy-MM-dd')} ${time}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC() -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const $ = cheerio.load(content) - - return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'bein.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + const [category] = channel.site_id.split('#') + const postid = channel.lang === 'ar' ? '25344' : '25356' + + return `https://www.bein.com/${ + channel.lang + }/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format( + 'YYYY-MM-DD' + )}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 }) + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + if (!title) return + const category = parseCategory($item) + const prev = programs[programs.length - 1] + let start = parseTime($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.plus({ days: 1 }) + } + prev.stop = start + } + let stop = parseTime($item, start) + if (stop < start) { + stop = stop.plus({ days: 1 }) + } + programs.push({ + title, + category, + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.title').text() +} + +function parseCategory($item) { + return $item('.format').text() +} + +function parseTime($item, date) { + let [, time] = $item('.time') + .text() + .match(/^(\d{2}:\d{2})/) || [null, null] + if (!time) return null + time = `${date.toFormat('yyyy-MM-dd')} ${time}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC() +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const $ = cheerio.load(content) + + return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() +} diff --git a/sites/bein.com/bein.com.test.js b/sites/bein.com/bein.com.test.js index 73174712a..0525afb09 100644 --- a/sites/bein.com/bein.com.test.js +++ b/sites/bein.com/bein.com.test.js @@ -1,60 +1,60 @@ -// npm run grab -- --site=bein.com - -const fs = require('fs') -const path = require('path') -const { parser, url } = require('./bein.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'entertainment#1', xmltv_id: 'beINMovies1Premiere.qa', lang: 'en' } - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&category=entertainment&cdate=2023-01-19&language=EN&loadindex=0&mins=00&offset=0&postid=25356&serviceidentity=bein.net' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve('sites/bein.com/__data__/content.html')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-18T20:15:00.000Z', - stop: '2023-01-18T22:15:00.000Z', - title: 'The Walk', - category: 'Movies' - }) - - expect(results[1]).toMatchObject({ - start: '2023-01-18T22:15:00.000Z', - stop: '2023-01-19T00:00:00.000Z', - title: 'Resident Evil: Welcome To Raccoon City', - category: 'Movies' - }) - - expect(results[10]).toMatchObject({ - start: '2023-01-19T15:30:00.000Z', - stop: '2023-01-19T18:00:00.000Z', - title: 'Spider-Man: No Way Home', - category: 'Movies' - }) -}) - -it('can handle empty guide', () => { - const noContent = fs.readFileSync(path.resolve('sites/bein.com/__data__/no-content.html')) - const result = parser({ - date, - channel, - content: noContent - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=bein.com + +const fs = require('fs') +const path = require('path') +const { parser, url } = require('./bein.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'entertainment#1', xmltv_id: 'beINMovies1Premiere.qa', lang: 'en' } + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&category=entertainment&cdate=2023-01-19&language=EN&loadindex=0&mins=00&offset=0&postid=25356&serviceidentity=bein.net' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve('sites/bein.com/__data__/content.html')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-18T20:15:00.000Z', + stop: '2023-01-18T22:15:00.000Z', + title: 'The Walk', + category: 'Movies' + }) + + expect(results[1]).toMatchObject({ + start: '2023-01-18T22:15:00.000Z', + stop: '2023-01-19T00:00:00.000Z', + title: 'Resident Evil: Welcome To Raccoon City', + category: 'Movies' + }) + + expect(results[10]).toMatchObject({ + start: '2023-01-19T15:30:00.000Z', + stop: '2023-01-19T18:00:00.000Z', + title: 'Spider-Man: No Way Home', + category: 'Movies' + }) +}) + +it('can handle empty guide', () => { + const noContent = fs.readFileSync(path.resolve('sites/bein.com/__data__/no-content.html')) + const result = parser({ + date, + channel, + content: noContent + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/beinsports.com/beinsports.com.config.js b/sites/beinsports.com/beinsports.com.config.js index 35e4783d4..2b443798b 100644 --- a/sites/beinsports.com/beinsports.com.config.js +++ b/sites/beinsports.com/beinsports.com.config.js @@ -1,130 +1,130 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'beinsports.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000, // 1h - interpretHeader: false - } - }, - url: function ({ date, channel }) { - let [region] = channel.site_id.split('#') - region = region ? `_${region}` : '' - - return `https://epg.beinsports.com/utctime${region}.php?mins=00&serviceidentity=beinsports.com&cdate=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - let i = 0 - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - if (!title) return - const category = parseCategory($item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (i === 0 && start.hour() > 18) { - date = date.subtract(1, 'd') - start = start.subtract(1, 'd') - } - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - let stop = parseStop($item, start) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - } - - programs.push({ title, category, start, stop }) - i++ - }) - - return programs - }, - async channels({ region, lang }) { - const suffix = region ? `_${region}` : '' - const content = await axios - .get( - `https://epg.beinsports.com/utctime${suffix}.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08` - ) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(content) - const items = $('.container > div, #epg_div > div').toArray() - return items - .map(item => { - const $item = cheerio.load(item) - const id = $item('*').attr('id') - if (!/^channels_[0-9]+$/.test(id)) return null - const channelId = id.replace('channels_', '') - const imgSrc = $item('img').attr('src') - const [, , name] = imgSrc.match(/(\/|)([a-z0-9-_.]+)(.png|.svg)$/i) || [null, null, ''] - - return { - lang, - site_id: `${region}#${channelId}`, - name - } - }) - .filter(i => i) - } -} - -function parseTitle($item) { - return $item('.title').text() -} - -function parseCategory($item) { - return $item('.format') - .map(function () { - return $item(this).text() - }) - .get() -} - -function parseStart($item, date) { - let time = $item('.time').text() - if (!time) return null - let [, start, period] = time.match(/^(\d{2}:\d{2})( AM| PM|)/) || [null, null, null] - if (!start) return null - start = `${date.format('YYYY-MM-DD')} ${start}${period}` - const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' - - return dayjs.tz(start, format, 'Asia/Qatar') -} - -function parseStop($item, date) { - let time = $item('.time').text() - if (!time) return null - let [, stop, period] = time.match(/(\d{2}:\d{2})( AM| PM|)$/) || [null, null, null] - if (!stop) return null - stop = `${date.format('YYYY-MM-DD')} ${stop}${period}` - const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' - - return dayjs.tz(stop, format, 'Asia/Qatar') -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const $ = cheerio.load(content) - - return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'beinsports.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000, // 1h + interpretHeader: false + } + }, + url: function ({ date, channel }) { + let [region] = channel.site_id.split('#') + region = region ? `_${region}` : '' + + return `https://epg.beinsports.com/utctime${region}.php?mins=00&serviceidentity=beinsports.com&cdate=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + let i = 0 + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + if (!title) return + const category = parseCategory($item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (i === 0 && start.hour() > 18) { + date = date.subtract(1, 'd') + start = start.subtract(1, 'd') + } + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + let stop = parseStop($item, start) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + } + + programs.push({ title, category, start, stop }) + i++ + }) + + return programs + }, + async channels({ region, lang }) { + const suffix = region ? `_${region}` : '' + const content = await axios + .get( + `https://epg.beinsports.com/utctime${suffix}.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08` + ) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(content) + const items = $('.container > div, #epg_div > div').toArray() + return items + .map(item => { + const $item = cheerio.load(item) + const id = $item('*').attr('id') + if (!/^channels_[0-9]+$/.test(id)) return null + const channelId = id.replace('channels_', '') + const imgSrc = $item('img').attr('src') + const [, , name] = imgSrc.match(/(\/|)([a-z0-9-_.]+)(.png|.svg)$/i) || [null, null, ''] + + return { + lang, + site_id: `${region}#${channelId}`, + name + } + }) + .filter(i => i) + } +} + +function parseTitle($item) { + return $item('.title').text() +} + +function parseCategory($item) { + return $item('.format') + .map(function () { + return $item(this).text() + }) + .get() +} + +function parseStart($item, date) { + let time = $item('.time').text() + if (!time) return null + let [, start, period] = time.match(/^(\d{2}:\d{2})( AM| PM|)/) || [null, null, null] + if (!start) return null + start = `${date.format('YYYY-MM-DD')} ${start}${period}` + const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' + + return dayjs.tz(start, format, 'Asia/Qatar') +} + +function parseStop($item, date) { + let time = $item('.time').text() + if (!time) return null + let [, stop, period] = time.match(/(\d{2}:\d{2})( AM| PM|)$/) || [null, null, null] + if (!stop) return null + stop = `${date.format('YYYY-MM-DD')} ${stop}${period}` + const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' + + return dayjs.tz(stop, format, 'Asia/Qatar') +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const $ = cheerio.load(content) + + return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() +} diff --git a/sites/beinsports.com/beinsports.com.test.js b/sites/beinsports.com/beinsports.com.test.js index a26de067b..0289cd561 100644 --- a/sites/beinsports.com/beinsports.com.test.js +++ b/sites/beinsports.com/beinsports.com.test.js @@ -1,91 +1,91 @@ -// npm run channels:parse -- --config=./sites/beinsports.com/beinsports.com.config.js --output=./sites/beinsports.com/beinsports.com_qa-ar.channels.xml --set=lang:ar --set=region:ar -// npm run grab -- --site=beinsports.com -// npm run grab -- --site=beinsports.com - -const { parser, url } = require('./beinsports.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-08', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '#2', xmltv_id: 'BeINSports.qa' } - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://epg.beinsports.com/utctime.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' - ) -}) - -it('can generate valid url for arabic guide', () => { - const channel = { site_id: 'ar#1', xmltv_id: 'BeINSports.qa' } - const result = url({ date, channel }) - expect(result).toBe( - 'https://epg.beinsports.com/utctime_ar.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content.html')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-07T19:30:00.000Z', - stop: '2022-05-07T21:20:00.000Z', - title: 'Lorient vs Marseille', - category: ['Ligue 1 2021/22'] - }) -}) - -it('can parse response for tomorrow', () => { - const date = dayjs.utc('2022-05-09', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync( - path.resolve('sites/beinsports.com/__data__/content_tomorrow.html') - ) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-08T21:20:00.000Z', - stop: '2022-05-08T23:10:00.000Z', - title: 'Celtic vs Hearts', - category: ['SPFL Premiership 2021/22'] - }) -}) - -it('can parse US response', () => { - const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content_us.html')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-07T20:00:00.000Z', - stop: '2022-05-07T22:00:00.000Z', - title: 'Basaksehir vs. Galatasaray', - category: ['Fútbol Turco Superliga', 'Soccer'] - }) -}) - -it('can handle empty guide', () => { - const noContent = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/no-content.html')) - const result = parser({ - date, - channel, - content: noContent - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/beinsports.com/beinsports.com.config.js --output=./sites/beinsports.com/beinsports.com_qa-ar.channels.xml --set=lang:ar --set=region:ar +// npm run grab -- --site=beinsports.com +// npm run grab -- --site=beinsports.com + +const { parser, url } = require('./beinsports.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-08', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '#2', xmltv_id: 'BeINSports.qa' } + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://epg.beinsports.com/utctime.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' + ) +}) + +it('can generate valid url for arabic guide', () => { + const channel = { site_id: 'ar#1', xmltv_id: 'BeINSports.qa' } + const result = url({ date, channel }) + expect(result).toBe( + 'https://epg.beinsports.com/utctime_ar.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content.html')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-07T19:30:00.000Z', + stop: '2022-05-07T21:20:00.000Z', + title: 'Lorient vs Marseille', + category: ['Ligue 1 2021/22'] + }) +}) + +it('can parse response for tomorrow', () => { + const date = dayjs.utc('2022-05-09', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync( + path.resolve('sites/beinsports.com/__data__/content_tomorrow.html') + ) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-08T21:20:00.000Z', + stop: '2022-05-08T23:10:00.000Z', + title: 'Celtic vs Hearts', + category: ['SPFL Premiership 2021/22'] + }) +}) + +it('can parse US response', () => { + const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content_us.html')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-07T20:00:00.000Z', + stop: '2022-05-07T22:00:00.000Z', + title: 'Basaksehir vs. Galatasaray', + category: ['Fútbol Turco Superliga', 'Soccer'] + }) +}) + +it('can handle empty guide', () => { + const noContent = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/no-content.html')) + const result = parser({ + date, + channel, + content: noContent + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.config.js b/sites/berrymedia.co.kr/berrymedia.co.kr.config.js index 773de12d8..63ab575a6 100644 --- a/sites/berrymedia.co.kr/berrymedia.co.kr.config.js +++ b/sites/berrymedia.co.kr/berrymedia.co.kr.config.js @@ -1,93 +1,93 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -dayjs.Ls.en.weekStart = 1 - -module.exports = { - site: 'berrymedia.co.kr', - days: 2, - url({ channel }) { - return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php` - }, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest' - }, - data({ date }) { - let params = new URLSearchParams() - let startOfWeek = date.startOf('week').format('YYYY-MM-DD') - let endOfWeek = date.endOf('week').format('YYYY-MM-DD') - - params.append('week', `${startOfWeek}~${endOfWeek}`) - params.append('day', date.format('YYYY-MM-DD')) - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const time = $item('span:nth-child(1)').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseTitle($item) { - return $item('span.sdfsdf').clone().children().remove().end().text().trim() -} - -function parseCategory($item) { - return $item('span:nth-child(2) > p').text().trim() -} - -function parseRating($item) { - const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.sc_time dd').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +dayjs.Ls.en.weekStart = 1 + +module.exports = { + site: 'berrymedia.co.kr', + days: 2, + url({ channel }) { + return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php` + }, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest' + }, + data({ date }) { + let params = new URLSearchParams() + let startOfWeek = date.startOf('week').format('YYYY-MM-DD') + let endOfWeek = date.endOf('week').format('YYYY-MM-DD') + + params.append('week', `${startOfWeek}~${endOfWeek}`) + params.append('day', date.format('YYYY-MM-DD')) + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const time = $item('span:nth-child(1)').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseTitle($item) { + return $item('span.sdfsdf').clone().children().remove().end().text().trim() +} + +function parseCategory($item) { + return $item('span:nth-child(2) > p').text().trim() +} + +function parseRating($item) { + const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.sc_time dd').toArray() +} diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.test.js b/sites/berrymedia.co.kr/berrymedia.co.kr.test.js index 88f98a75f..3c20e490b 100644 --- a/sites/berrymedia.co.kr/berrymedia.co.kr.test.js +++ b/sites/berrymedia.co.kr/berrymedia.co.kr.test.js @@ -1,79 +1,79 @@ -// npm run grab -- --site=berrymedia.co.kr - -const { parser, url, request } = require('./berrymedia.co.kr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '', - xmltv_id: 'GTV.kr' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php') -}) - -it('can generate request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest' - }) -}) - -it('can generate valid request data', () => { - let params = request.data({ date }) - - expect(params.get('week')).toBe('2023-01-23~2023-01-29') - expect(params.get('day')).toBe('2023-01-26') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-25T15:00:00.000Z', - stop: '2023-01-25T16:00:00.000Z', - title: '더트롯쇼', - category: '연예/오락', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[17]).toMatchObject({ - start: '2023-01-26T13:50:00.000Z', - stop: '2023-01-26T14:20:00.000Z', - title: '나는 자연인이다', - category: '교양', - rating: { - system: 'KMRB', - value: 'ALL' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=berrymedia.co.kr + +const { parser, url, request } = require('./berrymedia.co.kr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '', + xmltv_id: 'GTV.kr' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php') +}) + +it('can generate request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest' + }) +}) + +it('can generate valid request data', () => { + let params = request.data({ date }) + + expect(params.get('week')).toBe('2023-01-23~2023-01-29') + expect(params.get('day')).toBe('2023-01-26') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-25T15:00:00.000Z', + stop: '2023-01-25T16:00:00.000Z', + title: '더트롯쇼', + category: '연예/오락', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[17]).toMatchObject({ + start: '2023-01-26T13:50:00.000Z', + stop: '2023-01-26T14:20:00.000Z', + title: '나는 자연인이다', + category: '교양', + rating: { + system: 'KMRB', + value: 'ALL' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/bt.com/bt.com.config.js b/sites/bt.com/bt.com.config.js index 297bd80ca..2e7f77d11 100644 --- a/sites/bt.com/bt.com.config.js +++ b/sites/bt.com/bt.com.config.js @@ -1,54 +1,54 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'bt.com', - days: 2, - url: function ({ date, channel }) { - return `https://voila.metabroadcast.com/4/schedules/${ - channel.site_id - }.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=${date.utc().format()}&to=${date - .add(1, 'd') - .utc() - .format()}&source=api.youview.tv&annotations=content.description` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.item.title, - description: item.item.description, - icon: parseIcon(item), - season: parseSeason(item), - episode: parseEpisode(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseItems(content) { - const data = JSON.parse(content) - return data && data.schedule.entries ? data.schedule.entries : [] -} -function parseSeason(item) { - if (item.item.type !== 'episode') return null - return item.item.series_number || null -} -function parseEpisode(item) { - if (item.item.type !== 'episode') return null - return item.item.episode_number || null -} -function parseIcon(item) { - return item.item.image || null -} -function parseStart(item) { - return dayjs(item.broadcast.transmission_time) -} - -function parseStop(item) { - return dayjs(item.broadcast.transmission_end_time) -} +const dayjs = require('dayjs') + +module.exports = { + site: 'bt.com', + days: 2, + url: function ({ date, channel }) { + return `https://voila.metabroadcast.com/4/schedules/${ + channel.site_id + }.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=${date.utc().format()}&to=${date + .add(1, 'd') + .utc() + .format()}&source=api.youview.tv&annotations=content.description` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.item.title, + description: item.item.description, + icon: parseIcon(item), + season: parseSeason(item), + episode: parseEpisode(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseItems(content) { + const data = JSON.parse(content) + return data && data.schedule.entries ? data.schedule.entries : [] +} +function parseSeason(item) { + if (item.item.type !== 'episode') return null + return item.item.series_number || null +} +function parseEpisode(item) { + if (item.item.type !== 'episode') return null + return item.item.episode_number || null +} +function parseIcon(item) { + return item.item.image || null +} +function parseStart(item) { + return dayjs(item.broadcast.transmission_time) +} + +function parseStop(item) { + return dayjs(item.broadcast.transmission_end_time) +} diff --git a/sites/bt.com/bt.com.test.js b/sites/bt.com/bt.com.test.js index c7f93d65d..779b722cc 100644 --- a/sites/bt.com/bt.com.test.js +++ b/sites/bt.com/bt.com.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=bt.com - -const { parser, url } = require('./bt.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'hsxv', - xmltv_id: 'BBCOneHD.uk' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://voila.metabroadcast.com/4/schedules/hsxv.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=2022-03-20T00:00:00Z&to=2022-03-21T00:00:00Z&source=api.youview.tv&annotations=content.description' - ) -}) - -it('can parse response', () => { - const content = - '{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[{"broadcast":{"aliases":[{"namespace":"api.youview.tv:slot","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:event-locator","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:pcrid","value":"crid://fp.bbc.co.uk/b/3Q30S2"},{"namespace":"youview:schedule_event:id","value":"79d318f3-b41a-582d-b089-7b0172538b42"}],"transmission_time":"2022-03-19T23:30:00.000Z","transmission_end_time":"2022-03-20T01:20:00.000Z","broadcast_duration":6600,"broadcast_on":"hsxv","schedule_date":null,"repeat":null,"subtitled":true,"signed":null,"audio_described":false,"high_definition":null,"widescreen":null,"surround":null,"live":null,"premiere":null,"continuation":null,"new_series":null,"new_episode":null,"new_one_off":null,"revised_repeat":null,"blackout_restriction":{"all":false}},"item":{"id":"n72nsw","type":"item","display_title":{"title":"The Finest Hours (2016)","subtitle":null},"year":null,"media_type":"video","specialization":"tv","source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"title":"The Finest Hours (2016)","description":"Drama based on a true story, recounting one of history\'s most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.","image":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D","thumbnail":null,"duration":null,"container":null}}]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}' - - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'The Finest Hours (2016)', - description: - "Drama based on a true story, recounting one of history's most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.", - icon: 'https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D', - season: null, - episode: null, - start: '2022-03-19T23:30:00.000Z', - stop: '2022-03-20T01:20:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=bt.com + +const { parser, url } = require('./bt.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'hsxv', + xmltv_id: 'BBCOneHD.uk' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://voila.metabroadcast.com/4/schedules/hsxv.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=2022-03-20T00:00:00Z&to=2022-03-21T00:00:00Z&source=api.youview.tv&annotations=content.description' + ) +}) + +it('can parse response', () => { + const content = + '{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[{"broadcast":{"aliases":[{"namespace":"api.youview.tv:slot","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:event-locator","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:pcrid","value":"crid://fp.bbc.co.uk/b/3Q30S2"},{"namespace":"youview:schedule_event:id","value":"79d318f3-b41a-582d-b089-7b0172538b42"}],"transmission_time":"2022-03-19T23:30:00.000Z","transmission_end_time":"2022-03-20T01:20:00.000Z","broadcast_duration":6600,"broadcast_on":"hsxv","schedule_date":null,"repeat":null,"subtitled":true,"signed":null,"audio_described":false,"high_definition":null,"widescreen":null,"surround":null,"live":null,"premiere":null,"continuation":null,"new_series":null,"new_episode":null,"new_one_off":null,"revised_repeat":null,"blackout_restriction":{"all":false}},"item":{"id":"n72nsw","type":"item","display_title":{"title":"The Finest Hours (2016)","subtitle":null},"year":null,"media_type":"video","specialization":"tv","source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"title":"The Finest Hours (2016)","description":"Drama based on a true story, recounting one of history\'s most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.","image":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D","thumbnail":null,"duration":null,"container":null}}]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}' + + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'The Finest Hours (2016)', + description: + "Drama based on a true story, recounting one of history's most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.", + icon: 'https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D', + season: null, + episode: null, + start: '2022-03-19T23:30:00.000Z', + stop: '2022-03-20T01:20:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cablego.com.pe/cablego.com.pe.config.js b/sites/cablego.com.pe/cablego.com.pe.config.js index 1bc9eba51..5789fb379 100644 --- a/sites/cablego.com.pe/cablego.com.pe.config.js +++ b/sites/cablego.com.pe/cablego.com.pe.config.js @@ -1,109 +1,109 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'cablego.com.pe', - days: 2, - request: { - method: 'POST', - headers: { - 'x-requested-with': 'XMLHttpRequest' - }, - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ channel, date }) { - const [page] = channel.site_id.split('#') - - return `https://cablego.com.pe/epg/default/${date.format( - 'YYYY-MM-DD' - )}?page=${page}&do=loadPage` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const promises = [0, 1, 2, 3, 4].map(page => { - return axios.post( - `https://cablego.com.pe/epg/default/2022-11-28?page=${page}&do=loadPage`, - null, - { - headers: { - 'x-requested-with': 'XMLHttpRequest' - } - } - ) - }) - - const channels = [] - await Promise.allSettled(promises).then(results => { - results.forEach((r, page) => { - if (r.status === 'fulfilled') { - const html = r.value.data.snippets['snippet--channelGrid'] - const $ = cheerio.load(html) - $('.epg-channel-strip').each((i, el) => { - const channelId = $(el).find('.epg-channel-logo').attr('id') - channels.push({ - lang: 'es', - site_id: `${page}#${channelId}`, - name: $(el).find('img').attr('alt') - }) - }) - } - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('span:nth-child(2) > a').text().trim() -} - -function parseStart($item, date) { - const time = $item('.epg-show-start').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Lima') -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !data.snippets || !data.snippets['snippet--channelGrid']) return [] - const html = data.snippets['snippet--channelGrid'] - const $ = cheerio.load(html) - - return $(`#${channelId}`).parent().find('.epg-show').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'cablego.com.pe', + days: 2, + request: { + method: 'POST', + headers: { + 'x-requested-with': 'XMLHttpRequest' + }, + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ channel, date }) { + const [page] = channel.site_id.split('#') + + return `https://cablego.com.pe/epg/default/${date.format( + 'YYYY-MM-DD' + )}?page=${page}&do=loadPage` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const promises = [0, 1, 2, 3, 4].map(page => { + return axios.post( + `https://cablego.com.pe/epg/default/2022-11-28?page=${page}&do=loadPage`, + null, + { + headers: { + 'x-requested-with': 'XMLHttpRequest' + } + } + ) + }) + + const channels = [] + await Promise.allSettled(promises).then(results => { + results.forEach((r, page) => { + if (r.status === 'fulfilled') { + const html = r.value.data.snippets['snippet--channelGrid'] + const $ = cheerio.load(html) + $('.epg-channel-strip').each((i, el) => { + const channelId = $(el).find('.epg-channel-logo').attr('id') + channels.push({ + lang: 'es', + site_id: `${page}#${channelId}`, + name: $(el).find('img').attr('alt') + }) + }) + } + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('span:nth-child(2) > a').text().trim() +} + +function parseStart($item, date) { + const time = $item('.epg-show-start').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Lima') +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !data.snippets || !data.snippets['snippet--channelGrid']) return [] + const html = data.snippets['snippet--channelGrid'] + const $ = cheerio.load(html) + + return $(`#${channelId}`).parent().find('.epg-show').toArray() +} diff --git a/sites/cablego.com.pe/cablego.com.pe.test.js b/sites/cablego.com.pe/cablego.com.pe.test.js index c1da93e92..53faeaf93 100644 --- a/sites/cablego.com.pe/cablego.com.pe.test.js +++ b/sites/cablego.com.pe/cablego.com.pe.test.js @@ -1,54 +1,54 @@ -// npm run channels:parse -- --config=./sites/cablego.com.pe/cablego.com.pe.config.js --output=./sites/cablego.com.pe/cablego.com.pe.channels.xml -// npm run grab -- --site=cablego.com.pe - -const { parser, url, request } = require('./cablego.com.pe.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0#LATINA', - xmltv_id: 'Latina.pe' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://cablego.com.pe/epg/default/2022-11-28?page=0&do=loadPage' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-requested-with': 'XMLHttpRequest' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-28T05:00:00.000Z', - stop: '2022-11-28T06:30:00.000Z', - title: 'Especiales Qatar' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/cablego.com.pe/cablego.com.pe.config.js --output=./sites/cablego.com.pe/cablego.com.pe.channels.xml +// npm run grab -- --site=cablego.com.pe + +const { parser, url, request } = require('./cablego.com.pe.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0#LATINA', + xmltv_id: 'Latina.pe' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://cablego.com.pe/epg/default/2022-11-28?page=0&do=loadPage' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-requested-with': 'XMLHttpRequest' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-28T05:00:00.000Z', + stop: '2022-11-28T06:30:00.000Z', + title: 'Especiales Qatar' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cableplus.com.uy/cableplus.com.uy.config.js b/sites/cableplus.com.uy/cableplus.com.uy.config.js index a47a7bbce..017f6f45a 100644 --- a/sites/cableplus.com.uy/cableplus.com.uy.config.js +++ b/sites/cableplus.com.uy/cableplus.com.uy.config.js @@ -1,133 +1,133 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.reportv.com.ar/finder' - -module.exports = { - site: 'cableplus.com.uy', - days: 2, - url: `${API_ENDPOINT}/channel`, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ date, channel }) { - const params = new URLSearchParams() - params.append('idAlineacion', '3017') - params.append('idSenial', channel.site_id) - params.append('fecha', date.format('YYYY-MM-DD')) - params.append('hora', '00:00') - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - categories: parseCategories($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const params = new URLSearchParams({ idAlineacion: '3017' }) - const data = await axios - .post(`${API_ENDPOINT}/channelGrid`, params, { - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } - }) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(data) - - return $('.senial') - .map(function () { - return { - lang: 'es', - site_id: $(this).attr('id'), - name: $(this).find('img').attr('alt') - } - }) - .get() - } -} - -function parseTitle($item) { - return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child') - .text() - .trim() -} - -function parseIcon($item) { - return $item('img').data('src') || $item('img').attr('src') || null -} - -function parseCategories($item) { - return $item('p.evento_genero') - .map(function () { - return $item(this).text().trim() - }) - .toArray() -} - -function parseStart($item, date) { - let time = $item('.grid_fecha_hora').text().trim() - - if (time) { - return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo') - } - - time = $item('.fechaHora').text().trim() - - return time - ? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo') - : null -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa') - .filter(function () { - return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1 - }) - .toArray() - let otherItems = $('#owl-pc > .item-program') - .filter(function () { - return ( - $(this) - .find('.evento_titulo > .horario > p.fechaHora') - .text() - .indexOf(date.format('DD/MM')) > -1 - ) - }) - .toArray() - - return featuredItems.concat(otherItems) -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.reportv.com.ar/finder' + +module.exports = { + site: 'cableplus.com.uy', + days: 2, + url: `${API_ENDPOINT}/channel`, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ date, channel }) { + const params = new URLSearchParams() + params.append('idAlineacion', '3017') + params.append('idSenial', channel.site_id) + params.append('fecha', date.format('YYYY-MM-DD')) + params.append('hora', '00:00') + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + categories: parseCategories($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const params = new URLSearchParams({ idAlineacion: '3017' }) + const data = await axios + .post(`${API_ENDPOINT}/channelGrid`, params, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } + }) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(data) + + return $('.senial') + .map(function () { + return { + lang: 'es', + site_id: $(this).attr('id'), + name: $(this).find('img').attr('alt') + } + }) + .get() + } +} + +function parseTitle($item) { + return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child') + .text() + .trim() +} + +function parseIcon($item) { + return $item('img').data('src') || $item('img').attr('src') || null +} + +function parseCategories($item) { + return $item('p.evento_genero') + .map(function () { + return $item(this).text().trim() + }) + .toArray() +} + +function parseStart($item, date) { + let time = $item('.grid_fecha_hora').text().trim() + + if (time) { + return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo') + } + + time = $item('.fechaHora').text().trim() + + return time + ? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo') + : null +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa') + .filter(function () { + return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1 + }) + .toArray() + let otherItems = $('#owl-pc > .item-program') + .filter(function () { + return ( + $(this) + .find('.evento_titulo > .horario > p.fechaHora') + .text() + .indexOf(date.format('DD/MM')) > -1 + ) + }) + .toArray() + + return featuredItems.concat(otherItems) +} diff --git a/sites/cableplus.com.uy/cableplus.com.uy.test.js b/sites/cableplus.com.uy/cableplus.com.uy.test.js index 1608509db..bf00d7320 100644 --- a/sites/cableplus.com.uy/cableplus.com.uy.test.js +++ b/sites/cableplus.com.uy/cableplus.com.uy.test.js @@ -1,76 +1,76 @@ -// npm run channels:parse -- --config=./sites/cableplus.com.uy/cableplus.com.uy.config.js --output=./sites/cableplus.com.uy/cableplus.com.uy.channels.xml -// npm run grab -- --site=cableplus.com.uy - -const { parser, url, request } = require('./cableplus.com.uy.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-02-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2035', - xmltv_id: 'APlusV.uy' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.reportv.com.ar/finder/channel') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const params = request.data({ date, channel }) - - expect(params.get('idAlineacion')).toBe('3017') - expect(params.get('idSenial')).toBe('2035') - expect(params.get('fecha')).toBe('2023-02-12') - expect(params.get('hora')).toBe('00:00') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(21) - - expect(results[0]).toMatchObject({ - start: '2023-02-12T09:30:00.000Z', - stop: '2023-02-12T10:30:00.000Z', - title: 'Revista agropecuaria', - icon: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg', - categories: [] - }) - - expect(results[4]).toMatchObject({ - start: '2023-02-12T12:30:00.000Z', - stop: '2023-02-12T13:30:00.000Z', - title: 'De pago en pago', - icon: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg', - categories: ['Cultural'] - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/cableplus.com.uy/cableplus.com.uy.config.js --output=./sites/cableplus.com.uy/cableplus.com.uy.channels.xml +// npm run grab -- --site=cableplus.com.uy + +const { parser, url, request } = require('./cableplus.com.uy.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-02-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2035', + xmltv_id: 'APlusV.uy' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.reportv.com.ar/finder/channel') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const params = request.data({ date, channel }) + + expect(params.get('idAlineacion')).toBe('3017') + expect(params.get('idSenial')).toBe('2035') + expect(params.get('fecha')).toBe('2023-02-12') + expect(params.get('hora')).toBe('00:00') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(21) + + expect(results[0]).toMatchObject({ + start: '2023-02-12T09:30:00.000Z', + stop: '2023-02-12T10:30:00.000Z', + title: 'Revista agropecuaria', + icon: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg', + categories: [] + }) + + expect(results[4]).toMatchObject({ + start: '2023-02-12T12:30:00.000Z', + stop: '2023-02-12T13:30:00.000Z', + title: 'De pago en pago', + icon: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg', + categories: ['Cultural'] + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js b/sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js index 33595d2f7..47a5d7a9d 100644 --- a/sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js +++ b/sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js @@ -1,93 +1,93 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'canalplus-caraibes.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://service.canal-overseas.com/ott-frontend/vector/53001/channel/${channel.site_id}/events?filter.day=${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - if (item.title === 'Fin des programmes') return - const detail = await loadProgramDetails(item) - programs.push({ - title: item.title, - description: parseDescription(detail), - category: parseCategory(detail), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const html = await axios - .get('https://www.canalplus-caraibes.com/bl/guide-tv-ce-soir') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const script = $('body > script:nth-child(2)').html() - const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] - const data = JSON.parse(json) - const items = data.tvGuide.channels.byZapNumber - - return Object.values(items).map(item => { - return { - lang: 'fr', - site_id: item.epgID, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.onClick.URLPage) return {} - const url = item.onClick.URLPage - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseDescription(detail) { - return detail.detail.informations.summary || null -} - -function parseCategory(detail) { - return detail.detail.informations.subGenre || null -} -function parseIcon(item) { - return item.URLImage || item.URLImageDefault -} -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.timeSlices) return [] - const items = data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) - - return items -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'canalplus-caraibes.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://service.canal-overseas.com/ott-frontend/vector/53001/channel/${channel.site_id}/events?filter.day=${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + if (item.title === 'Fin des programmes') return + const detail = await loadProgramDetails(item) + programs.push({ + title: item.title, + description: parseDescription(detail), + category: parseCategory(detail), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const html = await axios + .get('https://www.canalplus-caraibes.com/bl/guide-tv-ce-soir') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const script = $('body > script:nth-child(2)').html() + const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] + const data = JSON.parse(json) + const items = data.tvGuide.channels.byZapNumber + + return Object.values(items).map(item => { + return { + lang: 'fr', + site_id: item.epgID, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.onClick.URLPage) return {} + const url = item.onClick.URLPage + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseDescription(detail) { + return detail.detail.informations.summary || null +} + +function parseCategory(detail) { + return detail.detail.informations.subGenre || null +} +function parseIcon(item) { + return item.URLImage || item.URLImageDefault +} +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.timeSlices) return [] + const items = data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) + + return items +} diff --git a/sites/canalplus-caraibes.com/canalplus-caraibes.com.test.js b/sites/canalplus-caraibes.com/canalplus-caraibes.com.test.js index d2041367a..42c1d5a7e 100644 --- a/sites/canalplus-caraibes.com/canalplus-caraibes.com.test.js +++ b/sites/canalplus-caraibes.com/canalplus-caraibes.com.test.js @@ -1,137 +1,137 @@ -// [Geo-blocked] node ./scripts/channels.js --config=./sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js --output=./sites/canalplus-caraibes.com/canalplus-caraibes.com.channels.xml -// npm run grab -- --site=canalplus-caraibes.com - -const { parser, url } = require('./canalplus-caraibes.com.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const channel = { - site_id: '50115', - xmltv_id: 'beINSports1France.fr' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=1' - ) -}) - -it('can parse response', done => { - const content = - '{"timeSlices":[{"contents":[{"title":"Rugby - Leinster / La Rochelle","subtitle":"Rugby","thirdTitle":"BEIN SPORTS 1 HD","startTime":1660815000,"endTime":1660816800,"onClick":{"displayTemplate":"miniDetail","displayName":"Rugby - Leinster / La Rochelle","URLPage":"https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765","URLVitrine":"https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"},"programID":224515801,"diffusionID":"140377765","URLImageDefault":"https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643","URLImage":"https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771"}],"timeSlice":"4"}]}' - axios.get.mockImplementation(url => { - if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765') { - return Promise.resolve({ - data: JSON.parse(`{ - "currentPage": { - "displayName": "Rugby - Leinster / La Rochelle", - "displayTemplate": "detailPage", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" - }, - "detail": { - "informations": { - "programmeType": "EPG", - "isInOffer": false, - "isInOfferOnDevice": false, - "isInOfferForD2G": false, - "availableInVoDOnDevice": false, - "availableInVoDOnG5": false, - "availableInD2GOnDevice": false, - "availableInLiveOnDevice": false, - "rediffusions": true, - "canBeRecorded": false, - "channelName": "BEIN SPORTS 1 HD", - "startTime": 1660815000, - "endTime": 1660816800, - "title": "Rugby - Leinster / La Rochelle", - "subtitle": "Rugby", - "thirdTitle": "BEIN SPORTS 1 HD", - "genre": "Sport", - "subGenre": "Rugby", - "editorialTitle": "Sport, France, 0h30", - "audioLanguage": "VF", - "summary": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.", - "summaryMedium": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.", - "programID": 224515801, - "sharingURL": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", - "EpgId": 50115, - "CSA": 1, - "HD": false, - "3D": false, - "diffusionID": "140377765", - "duration": "1800", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" - }, - "diffusions": [ - { - "diffusionDateUTC": 1660815000, - "sharingUrl": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", - "broadcastId": "140377765", - "name": "BEIN SPORTS 1 HD", - "epgID": "50115", - "ZapNumber": "191", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e" - } - ] - } - }`) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-18T09:30:00.000Z', - stop: '2022-08-18T10:00:00.000Z', - title: 'Rugby - Leinster / La Rochelle', - icon: 'https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771', - category: 'Rugby', - description: - "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995." - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: - '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// [Geo-blocked] node ./scripts/channels.js --config=./sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js --output=./sites/canalplus-caraibes.com/canalplus-caraibes.com.channels.xml +// npm run grab -- --site=canalplus-caraibes.com + +const { parser, url } = require('./canalplus-caraibes.com.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const channel = { + site_id: '50115', + xmltv_id: 'beINSports1France.fr' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=1' + ) +}) + +it('can parse response', done => { + const content = + '{"timeSlices":[{"contents":[{"title":"Rugby - Leinster / La Rochelle","subtitle":"Rugby","thirdTitle":"BEIN SPORTS 1 HD","startTime":1660815000,"endTime":1660816800,"onClick":{"displayTemplate":"miniDetail","displayName":"Rugby - Leinster / La Rochelle","URLPage":"https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765","URLVitrine":"https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"},"programID":224515801,"diffusionID":"140377765","URLImageDefault":"https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643","URLImage":"https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771"}],"timeSlice":"4"}]}' + axios.get.mockImplementation(url => { + if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765') { + return Promise.resolve({ + data: JSON.parse(`{ + "currentPage": { + "displayName": "Rugby - Leinster / La Rochelle", + "displayTemplate": "detailPage", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" + }, + "detail": { + "informations": { + "programmeType": "EPG", + "isInOffer": false, + "isInOfferOnDevice": false, + "isInOfferForD2G": false, + "availableInVoDOnDevice": false, + "availableInVoDOnG5": false, + "availableInD2GOnDevice": false, + "availableInLiveOnDevice": false, + "rediffusions": true, + "canBeRecorded": false, + "channelName": "BEIN SPORTS 1 HD", + "startTime": 1660815000, + "endTime": 1660816800, + "title": "Rugby - Leinster / La Rochelle", + "subtitle": "Rugby", + "thirdTitle": "BEIN SPORTS 1 HD", + "genre": "Sport", + "subGenre": "Rugby", + "editorialTitle": "Sport, France, 0h30", + "audioLanguage": "VF", + "summary": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.", + "summaryMedium": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.", + "programID": 224515801, + "sharingURL": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", + "EpgId": 50115, + "CSA": 1, + "HD": false, + "3D": false, + "diffusionID": "140377765", + "duration": "1800", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" + }, + "diffusions": [ + { + "diffusionDateUTC": 1660815000, + "sharingUrl": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", + "broadcastId": "140377765", + "name": "BEIN SPORTS 1 HD", + "epgID": "50115", + "ZapNumber": "191", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e" + } + ] + } + }`) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-18T09:30:00.000Z', + stop: '2022-08-18T10:00:00.000Z', + title: 'Rugby - Leinster / La Rochelle', + icon: 'https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771', + category: 'Rugby', + description: + "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995." + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: + '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/canalplus-haiti.com/canalplus-haiti.com.config.js b/sites/canalplus-haiti.com/canalplus-haiti.com.config.js index 86b1aff5c..f9ba119f5 100644 --- a/sites/canalplus-haiti.com/canalplus-haiti.com.config.js +++ b/sites/canalplus-haiti.com/canalplus-haiti.com.config.js @@ -1,94 +1,94 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'canalplus-haiti.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://service.canal-overseas.com/ott-frontend/vector/53101/channel/${channel.site_id}/events?filter.day=${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - if (item.title === 'Fin des programmes') return - const detail = await loadProgramDetails(item) - programs.push({ - title: item.title, - description: parseDescription(detail), - category: parseCategory(detail), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const html = await axios - .get('https://www.canalplus-haiti.com/guide-tv-ce-soir') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const script = $('body > script:nth-child(2)').html() - const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] - const data = JSON.parse(json) - const items = data.tvGuide.channels.byZapNumber - - return Object.values(items).map(item => { - return { - lang: 'fr', - site_id: item.epgID, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.onClick.URLPage) return {} - const url = item.onClick.URLPage - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseDescription(detail) { - return detail.detail.informations.summary || null -} - -function parseCategory(detail) { - return detail.detail.informations.subGenre || null -} -function parseIcon(item) { - return item.URLImage || item.URLImageDefault -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.timeSlices) return [] - const items = data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) - - return items -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'canalplus-haiti.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://service.canal-overseas.com/ott-frontend/vector/53101/channel/${channel.site_id}/events?filter.day=${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + if (item.title === 'Fin des programmes') return + const detail = await loadProgramDetails(item) + programs.push({ + title: item.title, + description: parseDescription(detail), + category: parseCategory(detail), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const html = await axios + .get('https://www.canalplus-haiti.com/guide-tv-ce-soir') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const script = $('body > script:nth-child(2)').html() + const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] + const data = JSON.parse(json) + const items = data.tvGuide.channels.byZapNumber + + return Object.values(items).map(item => { + return { + lang: 'fr', + site_id: item.epgID, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.onClick.URLPage) return {} + const url = item.onClick.URLPage + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseDescription(detail) { + return detail.detail.informations.summary || null +} + +function parseCategory(detail) { + return detail.detail.informations.subGenre || null +} +function parseIcon(item) { + return item.URLImage || item.URLImageDefault +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.timeSlices) return [] + const items = data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) + + return items +} diff --git a/sites/canalplus-haiti.com/canalplus-haiti.com.test.js b/sites/canalplus-haiti.com/canalplus-haiti.com.test.js index 16d1d1d60..4a05e35d9 100644 --- a/sites/canalplus-haiti.com/canalplus-haiti.com.test.js +++ b/sites/canalplus-haiti.com/canalplus-haiti.com.test.js @@ -1,176 +1,176 @@ -// [Geo-blocked] npm run channels:parse -- --config=./sites/canalplus-haiti.com/canalplus-haiti.com.config.js --output=./sites/canalplus-haiti.com/canalplus-haiti.com.channels.xml -// npm run grab -- --site=canalplus-haiti.com - -const { parser, url } = require('./canalplus-haiti.com.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const channel = { - site_id: '51006', - xmltv_id: 'ViaATV.mq' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=1' - ) -}) - -it('can parse response', done => { - const content = `{ - "timeSlices": [ - { - "contents": [ - { - "title": "New Amsterdam - S3 - Ep7", - "subtitle": "Episode 7 - Le mur de la honte", - "thirdTitle": "viaATV", - "startTime": 1660780500, - "endTime": 1660783200, - "onClick": { - "displayTemplate": "miniDetail", - "displayName": "New Amsterdam - S3 - Ep7", - "URLPage": "https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" - }, - "programID": 187882282, - "diffusionID": "140952809", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e" - } - ], - "timeSlice": "2" - } - ] - }` - axios.get.mockImplementation(url => { - if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809') { - return Promise.resolve({ - data: JSON.parse(`{ - "currentPage": { - "displayName": "New Amsterdam - S3 - Ep7", - "displayTemplate": "detailPage", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" - }, - "detail": { - "informations": { - "programmeType": "EPG", - "isInOffer": false, - "isInOfferOnDevice": false, - "isInOfferForD2G": false, - "availableInVoDOnDevice": false, - "availableInVoDOnG5": false, - "availableInD2GOnDevice": false, - "availableInLiveOnDevice": false, - "rediffusions": true, - "canBeRecorded": false, - "channelName": "viaATV", - "startTime": 1660780500, - "endTime": 1660783200, - "title": "New Amsterdam - S3 - Ep7", - "subtitle": "Episode 7 - Le mur de la honte", - "thirdTitle": "viaATV", - "genre": "Séries", - "subGenre": "Série Hôpital", - "editorialTitle": "Séries, Etats-Unis, 2020, 0h45", - "audioLanguage": "VF", - "personnalities": [ - { - "prefix": "De :", - "content": "Darnell Martin" - }, - { - "prefix": "Avec :", - "content": "André De Shields, Anna Suzuki, Anupam Kher, Baylen Thomas, Christine Chang, Craig Wedren, Daniel Dae Kim, Dierdre Friel, Em Grosland, Emma Ramos, Freema Agyeman, Gina Gershon, Graham Norris, Jamie Ann Romero, Janet Montgomery, Jefferson Friedman, Joshua Gitta, Kerry Flanagan, Larry Bryggman, Mike Doyle, Nora Clow, Opal Clow, Ryan Eggold, Simone Policano, Stephen Spinella, Tyler Labine" - } - ], - "summary": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.", - "summaryMedium": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.", - "programID": 187882282, - "sharingURL": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam-s3-ep7.html", - "labels": { - "allocine": false, - "telerama": false, - "sensCritique": false - }, - "EpgId": 51006, - "CSA": 1, - "HD": false, - "3D": false, - "diffusionID": "140952809", - "duration": "2700", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" - }, - "diffusions": [ - { - "diffusionDateUTC": 1660780500, - "sharingUrl": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam.html", - "broadcastId": "140952809", - "name": "viaATV", - "epgID": "51006", - "ZapNumber": "28", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce" - } - ] - } - }`) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-17T23:55:00.000Z', - stop: '2022-08-18T00:40:00.000Z', - title: 'New Amsterdam - S3 - Ep7', - icon: 'https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e', - category: 'Série Hôpital', - description: - "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam." - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: - '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// [Geo-blocked] npm run channels:parse -- --config=./sites/canalplus-haiti.com/canalplus-haiti.com.config.js --output=./sites/canalplus-haiti.com/canalplus-haiti.com.channels.xml +// npm run grab -- --site=canalplus-haiti.com + +const { parser, url } = require('./canalplus-haiti.com.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const channel = { + site_id: '51006', + xmltv_id: 'ViaATV.mq' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=1' + ) +}) + +it('can parse response', done => { + const content = `{ + "timeSlices": [ + { + "contents": [ + { + "title": "New Amsterdam - S3 - Ep7", + "subtitle": "Episode 7 - Le mur de la honte", + "thirdTitle": "viaATV", + "startTime": 1660780500, + "endTime": 1660783200, + "onClick": { + "displayTemplate": "miniDetail", + "displayName": "New Amsterdam - S3 - Ep7", + "URLPage": "https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" + }, + "programID": 187882282, + "diffusionID": "140952809", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e" + } + ], + "timeSlice": "2" + } + ] + }` + axios.get.mockImplementation(url => { + if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809') { + return Promise.resolve({ + data: JSON.parse(`{ + "currentPage": { + "displayName": "New Amsterdam - S3 - Ep7", + "displayTemplate": "detailPage", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" + }, + "detail": { + "informations": { + "programmeType": "EPG", + "isInOffer": false, + "isInOfferOnDevice": false, + "isInOfferForD2G": false, + "availableInVoDOnDevice": false, + "availableInVoDOnG5": false, + "availableInD2GOnDevice": false, + "availableInLiveOnDevice": false, + "rediffusions": true, + "canBeRecorded": false, + "channelName": "viaATV", + "startTime": 1660780500, + "endTime": 1660783200, + "title": "New Amsterdam - S3 - Ep7", + "subtitle": "Episode 7 - Le mur de la honte", + "thirdTitle": "viaATV", + "genre": "Séries", + "subGenre": "Série Hôpital", + "editorialTitle": "Séries, Etats-Unis, 2020, 0h45", + "audioLanguage": "VF", + "personnalities": [ + { + "prefix": "De :", + "content": "Darnell Martin" + }, + { + "prefix": "Avec :", + "content": "André De Shields, Anna Suzuki, Anupam Kher, Baylen Thomas, Christine Chang, Craig Wedren, Daniel Dae Kim, Dierdre Friel, Em Grosland, Emma Ramos, Freema Agyeman, Gina Gershon, Graham Norris, Jamie Ann Romero, Janet Montgomery, Jefferson Friedman, Joshua Gitta, Kerry Flanagan, Larry Bryggman, Mike Doyle, Nora Clow, Opal Clow, Ryan Eggold, Simone Policano, Stephen Spinella, Tyler Labine" + } + ], + "summary": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.", + "summaryMedium": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.", + "programID": 187882282, + "sharingURL": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam-s3-ep7.html", + "labels": { + "allocine": false, + "telerama": false, + "sensCritique": false + }, + "EpgId": 51006, + "CSA": 1, + "HD": false, + "3D": false, + "diffusionID": "140952809", + "duration": "2700", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" + }, + "diffusions": [ + { + "diffusionDateUTC": 1660780500, + "sharingUrl": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam.html", + "broadcastId": "140952809", + "name": "viaATV", + "epgID": "51006", + "ZapNumber": "28", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce" + } + ] + } + }`) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-17T23:55:00.000Z', + stop: '2022-08-18T00:40:00.000Z', + title: 'New Amsterdam - S3 - Ep7', + icon: 'https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e', + category: 'Série Hôpital', + description: + "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam." + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: + '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/canalplus-reunion.com/canalplus-reunion.com.config.js b/sites/canalplus-reunion.com/canalplus-reunion.com.config.js index 218e8333d..54032a86a 100644 --- a/sites/canalplus-reunion.com/canalplus-reunion.com.config.js +++ b/sites/canalplus-reunion.com/canalplus-reunion.com.config.js @@ -1,72 +1,72 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'canalplus-reunion.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://service.canal-overseas.com/ott-frontend/vector/63001/channel/${channel.site_id}/events?filter.day=${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - if (item.title === 'Fin des programmes') return - const detail = await loadProgramDetails(item) - programs.push({ - title: item.title, - description: parseDescription(detail), - category: parseCategory(detail), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - } -} - -async function loadProgramDetails(item) { - if (!item.onClick.URLPage) return {} - const url = item.onClick.URLPage - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseDescription(detail) { - return detail.detail.informations.summary || null -} - -function parseCategory(detail) { - return detail.detail.informations.subGenre || null -} -function parseIcon(item) { - return item.URLImage || item.URLImageDefault -} -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.timeSlices) return [] - const items = data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) - - return items -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'canalplus-reunion.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://service.canal-overseas.com/ott-frontend/vector/63001/channel/${channel.site_id}/events?filter.day=${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + if (item.title === 'Fin des programmes') return + const detail = await loadProgramDetails(item) + programs.push({ + title: item.title, + description: parseDescription(detail), + category: parseCategory(detail), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + } +} + +async function loadProgramDetails(item) { + if (!item.onClick.URLPage) return {} + const url = item.onClick.URLPage + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseDescription(detail) { + return detail.detail.informations.summary || null +} + +function parseCategory(detail) { + return detail.detail.informations.subGenre || null +} +function parseIcon(item) { + return item.URLImage || item.URLImageDefault +} +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.timeSlices) return [] + const items = data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) + + return items +} diff --git a/sites/canalplus-reunion.com/canalplus-reunion.com.test.js b/sites/canalplus-reunion.com/canalplus-reunion.com.test.js index c6b2cb393..cb5217956 100644 --- a/sites/canalplus-reunion.com/canalplus-reunion.com.test.js +++ b/sites/canalplus-reunion.com/canalplus-reunion.com.test.js @@ -1,160 +1,160 @@ -// npm run grab -- --site=canalplus-reunion.com - -const { parser, url } = require('./canalplus-reunion.com.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const channel = { - site_id: '60243', - xmltv_id: 'beINSports2France.fr' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=1' - ) -}) - -it('can parse response', done => { - const content = `{ - "timeSlices": [ - { - "contents": [ - { - "title": "Almeria / Real Madrid", - "subtitle": "Football", - "thirdTitle": "BEIN SPORTS 2 HD", - "startTime": 1660780800, - "endTime": 1660788000, - "onClick": { - "displayTemplate": "miniDetail", - "displayName": "Almeria / Real Madrid", - "URLPage": "https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" - }, - "programID": 224523053, - "diffusionID": "140382363", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20" - } - ], - "timeSlice": "4" - } - ] - }` - axios.get.mockImplementation(url => { - if (url === 'https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363') { - return Promise.resolve({ - data: JSON.parse(`{ - "currentPage": { - "displayName": "Almeria / Real Madrid", - "displayTemplate": "detailPage", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" - }, - "detail": { - "informations": { - "programmeType": "EPG", - "isInOffer": false, - "isInOfferOnDevice": false, - "isInOfferForD2G": false, - "availableInVoDOnDevice": false, - "availableInVoDOnG5": false, - "availableInD2GOnDevice": false, - "availableInLiveOnDevice": false, - "rediffusions": true, - "canBeRecorded": false, - "channelName": "BEIN SPORTS 2 HD", - "startTime": 1660780800, - "endTime": 1660788000, - "title": "Almeria / Real Madrid", - "subtitle": "Football", - "thirdTitle": "BEIN SPORTS 2 HD", - "genre": "Sport", - "subGenre": "Football", - "editorialTitle": "Sport, Espagne, 2h00", - "audioLanguage": "VF", - "summary": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.", - "summaryMedium": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.", - "programID": 224523053, - "sharingURL": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", - "EpgId": 60243, - "CSA": 1, - "HD": false, - "3D": false, - "diffusionID": "140382363", - "duration": "7200", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" - }, - "diffusions": [ - { - "diffusionDateUTC": 1660780800, - "sharingUrl": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", - "broadcastId": "140382363", - "name": "BEIN SPORTS 2 HD", - "epgID": "60243", - "ZapNumber": "96", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9" - } - ] - } - }`) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-18T00:00:00.000Z', - stop: '2022-08-18T02:00:00.000Z', - title: 'Almeria / Real Madrid', - icon: 'https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20', - category: 'Football', - description: - "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date." - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: - '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run grab -- --site=canalplus-reunion.com + +const { parser, url } = require('./canalplus-reunion.com.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const channel = { + site_id: '60243', + xmltv_id: 'beINSports2France.fr' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=1' + ) +}) + +it('can parse response', done => { + const content = `{ + "timeSlices": [ + { + "contents": [ + { + "title": "Almeria / Real Madrid", + "subtitle": "Football", + "thirdTitle": "BEIN SPORTS 2 HD", + "startTime": 1660780800, + "endTime": 1660788000, + "onClick": { + "displayTemplate": "miniDetail", + "displayName": "Almeria / Real Madrid", + "URLPage": "https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" + }, + "programID": 224523053, + "diffusionID": "140382363", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20" + } + ], + "timeSlice": "4" + } + ] + }` + axios.get.mockImplementation(url => { + if (url === 'https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363') { + return Promise.resolve({ + data: JSON.parse(`{ + "currentPage": { + "displayName": "Almeria / Real Madrid", + "displayTemplate": "detailPage", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" + }, + "detail": { + "informations": { + "programmeType": "EPG", + "isInOffer": false, + "isInOfferOnDevice": false, + "isInOfferForD2G": false, + "availableInVoDOnDevice": false, + "availableInVoDOnG5": false, + "availableInD2GOnDevice": false, + "availableInLiveOnDevice": false, + "rediffusions": true, + "canBeRecorded": false, + "channelName": "BEIN SPORTS 2 HD", + "startTime": 1660780800, + "endTime": 1660788000, + "title": "Almeria / Real Madrid", + "subtitle": "Football", + "thirdTitle": "BEIN SPORTS 2 HD", + "genre": "Sport", + "subGenre": "Football", + "editorialTitle": "Sport, Espagne, 2h00", + "audioLanguage": "VF", + "summary": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.", + "summaryMedium": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.", + "programID": 224523053, + "sharingURL": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", + "EpgId": 60243, + "CSA": 1, + "HD": false, + "3D": false, + "diffusionID": "140382363", + "duration": "7200", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" + }, + "diffusions": [ + { + "diffusionDateUTC": 1660780800, + "sharingUrl": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", + "broadcastId": "140382363", + "name": "BEIN SPORTS 2 HD", + "epgID": "60243", + "ZapNumber": "96", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9" + } + ] + } + }`) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-18T00:00:00.000Z', + stop: '2022-08-18T02:00:00.000Z', + title: 'Almeria / Real Madrid', + icon: 'https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20', + category: 'Football', + description: + "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date." + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: + '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/canalplus.com/canalplus.com.config.js b/sites/canalplus.com/canalplus.com.config.js index 1d719e266..19e429065 100644 --- a/sites/canalplus.com/canalplus.com.config.js +++ b/sites/canalplus.com/canalplus.com.config.js @@ -1,185 +1,185 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -module.exports = { - site: 'canalplus.com', - days: 2, - url: async function ({ channel, date }) { - const [region, site_id] = channel.site_id.split('#') - const data = await axios - .get(`https://www.canalplus.com/${region}/programme-tv/`) - .then(r => r.data.toString()) - .catch(err => console.log(err)) - const token = parseToken(data) - - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://hodor.canalplus.pro/api/v2/mycanal/channels/${token}/${site_id}/broadcasts/day/${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const details = await loadProgramDetails(item) - const info = parseInfo(details) - const start = parseStart(item) - if (prev) prev.stop = start - const stop = start.add(1, 'h') - programs.push({ - title: item.title, - description: parseDescription(info), - icon: parseIcon(info), - actors: parseCast(info, 'Avec :'), - director: parseCast(info, 'De :'), - writer: parseCast(info, 'Scénario :'), - composer: parseCast(info, 'Musique :'), - presenter: parseCast(info, 'Présenté par :'), - date: parseDate(info), - rating: parseRating(info), - start, - stop - }) - } - - return programs - }, - async channels() { - const endpoints = { - ad: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ad/all/v2.2/globalchannels.json', - bf: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bf/all/v2.2/globalchannels.json', - bi: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bi/all/v2.2/globalchannels.json', - bj: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bj/all/v2.2/globalchannels.json', - bl: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/bl/all/v2.2/globalchannels.json', - cd: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cd/all/v2.2/globalchannels.json', - cf: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cf/all/v2.2/globalchannels.json', - cg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cg/all/v2.2/globalchannels.json', - ch: 'https://secure-webtv-static.canal-plus.com/metadata/cpche/all/v2.2/globalchannels.json', - ci: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ci/all/v2.2/globalchannels.json', - cm: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cm/all/v2.2/globalchannels.json', - cv: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cv/all/v2.2/globalchannels.json', - dj: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/dj/all/v2.2/globalchannels.json', - fr: 'https://secure-webtv-static.canal-plus.com/metadata/cpfra/all/v2.2/globalchannels.json', - ga: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ga/all/v2.2/globalchannels.json', - gf: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/gf/all/v2.2/globalchannels.json', - gh: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gh/all/v2.2/globalchannels.json', - gm: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gm/all/v2.2/globalchannels.json', - gn: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gn/all/v2.2/globalchannels.json', - gp: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gp/all/v2.2/globalchannels.json', - gw: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gw/all/v2.2/globalchannels.json', - mf: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/mf/all/v2.2/globalchannels.json', - mg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/mg/all/v2.2/globalchannels.json', - ml: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ml/all/v2.2/globalchannels.json', - mq: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/mq/all/v2.2/globalchannels.json', - mr: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/mr/all/v2.2/globalchannels.json', - mu: 'https://secure-webtv-static.canal-plus.com/metadata/cpmus/mu/all/v2.2/globalchannels.json', - nc: 'https://secure-webtv-static.canal-plus.com/metadata/cpncl/nc/all/v2.2/globalchannels.json', - ne: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ne/all/v2.2/globalchannels.json', - pl: 'https://secure-webtv-static.canal-plus.com/metadata/cppol/all/v2.2/globalchannels.json', - re: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/re/all/v2.2/globalchannels.json', - rw: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/rw/all/v2.2/globalchannels.json', - sl: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/sl/all/v2.2/globalchannels.json', - sn: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/sn/all/v2.2/globalchannels.json', - td: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/td/all/v2.2/globalchannels.json', - tg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/tg/all/v2.2/globalchannels.json', - wf: 'https://secure-webtv-static.canal-plus.com/metadata/cpncl/wf/all/v2.2/globalchannels.json', - yt: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/yt/all/v2.2/globalchannels.json' - } - - let channels = [] - for (let [region, url] of Object.entries(endpoints)) { - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - data.channels.forEach(channel => { - const site_id = region === 'fr' ? `#${channel.id}` : `${region}#${channel.id}` - - if (channel.name === '.') return - - channels.push({ - lang: 'fr', - site_id, - name: channel.name - }) - }) - } - - return channels - } -} - -function parseToken(data) { - const [, token] = data.match(/"token":"([^"]+)/) || [null, null] - - return token -} - -function parseStart(item) { - return item && item.startTime ? dayjs(item.startTime) : null -} - -function parseIcon(info) { - return info ? info.URLImage : null -} - -function parseDescription(info) { - return info ? info.summary : null -} - -function parseInfo(data) { - if (!data || !data.detail || !data.detail.informations) return null - - return data.detail.informations -} - -async function loadProgramDetails(item) { - if (!item.onClick || !item.onClick.URLPage) return {} - - return await axios - .get(item.onClick.URLPage) - .then(r => r.data) - .catch(console.error) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.timeSlices)) return [] - - return data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) -} - -function parseCast(info, type) { - let people = [] - if (info && info.personnalities) { - const personnalities = info.personnalities.find(i => i.prefix == type) - if (!personnalities) return people - for (let person of personnalities.personnalitiesList) { - people.push(person.title) - } - } - return people -} - -function parseDate(info) { - return info && info.productionYear ? info.productionYear : null -} - -function parseRating(info) { - if (!info || !info.parentalRatings) return null - let rating = info.parentalRatings.find(i => i.authority === 'CSA') - if (!rating || Array.isArray(rating)) return null - if (rating.value === '1') return null - if (rating.value === '2') rating.value = '-10' - if (rating.value === '3') rating.value = '-12' - if (rating.value === '4') rating.value = '-16' - if (rating.value === '5') rating.value = '-18' - return { - system: rating.authority, - value: rating.value - } -} +const dayjs = require('dayjs') +const axios = require('axios') + +module.exports = { + site: 'canalplus.com', + days: 2, + url: async function ({ channel, date }) { + const [region, site_id] = channel.site_id.split('#') + const data = await axios + .get(`https://www.canalplus.com/${region}/programme-tv/`) + .then(r => r.data.toString()) + .catch(err => console.log(err)) + const token = parseToken(data) + + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://hodor.canalplus.pro/api/v2/mycanal/channels/${token}/${site_id}/broadcasts/day/${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const details = await loadProgramDetails(item) + const info = parseInfo(details) + const start = parseStart(item) + if (prev) prev.stop = start + const stop = start.add(1, 'h') + programs.push({ + title: item.title, + description: parseDescription(info), + icon: parseIcon(info), + actors: parseCast(info, 'Avec :'), + director: parseCast(info, 'De :'), + writer: parseCast(info, 'Scénario :'), + composer: parseCast(info, 'Musique :'), + presenter: parseCast(info, 'Présenté par :'), + date: parseDate(info), + rating: parseRating(info), + start, + stop + }) + } + + return programs + }, + async channels() { + const endpoints = { + ad: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ad/all/v2.2/globalchannels.json', + bf: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bf/all/v2.2/globalchannels.json', + bi: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bi/all/v2.2/globalchannels.json', + bj: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bj/all/v2.2/globalchannels.json', + bl: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/bl/all/v2.2/globalchannels.json', + cd: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cd/all/v2.2/globalchannels.json', + cf: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cf/all/v2.2/globalchannels.json', + cg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cg/all/v2.2/globalchannels.json', + ch: 'https://secure-webtv-static.canal-plus.com/metadata/cpche/all/v2.2/globalchannels.json', + ci: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ci/all/v2.2/globalchannels.json', + cm: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cm/all/v2.2/globalchannels.json', + cv: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cv/all/v2.2/globalchannels.json', + dj: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/dj/all/v2.2/globalchannels.json', + fr: 'https://secure-webtv-static.canal-plus.com/metadata/cpfra/all/v2.2/globalchannels.json', + ga: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ga/all/v2.2/globalchannels.json', + gf: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/gf/all/v2.2/globalchannels.json', + gh: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gh/all/v2.2/globalchannels.json', + gm: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gm/all/v2.2/globalchannels.json', + gn: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gn/all/v2.2/globalchannels.json', + gp: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gp/all/v2.2/globalchannels.json', + gw: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gw/all/v2.2/globalchannels.json', + mf: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/mf/all/v2.2/globalchannels.json', + mg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/mg/all/v2.2/globalchannels.json', + ml: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ml/all/v2.2/globalchannels.json', + mq: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/mq/all/v2.2/globalchannels.json', + mr: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/mr/all/v2.2/globalchannels.json', + mu: 'https://secure-webtv-static.canal-plus.com/metadata/cpmus/mu/all/v2.2/globalchannels.json', + nc: 'https://secure-webtv-static.canal-plus.com/metadata/cpncl/nc/all/v2.2/globalchannels.json', + ne: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ne/all/v2.2/globalchannels.json', + pl: 'https://secure-webtv-static.canal-plus.com/metadata/cppol/all/v2.2/globalchannels.json', + re: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/re/all/v2.2/globalchannels.json', + rw: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/rw/all/v2.2/globalchannels.json', + sl: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/sl/all/v2.2/globalchannels.json', + sn: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/sn/all/v2.2/globalchannels.json', + td: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/td/all/v2.2/globalchannels.json', + tg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/tg/all/v2.2/globalchannels.json', + wf: 'https://secure-webtv-static.canal-plus.com/metadata/cpncl/wf/all/v2.2/globalchannels.json', + yt: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/yt/all/v2.2/globalchannels.json' + } + + let channels = [] + for (let [region, url] of Object.entries(endpoints)) { + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + data.channels.forEach(channel => { + const site_id = region === 'fr' ? `#${channel.id}` : `${region}#${channel.id}` + + if (channel.name === '.') return + + channels.push({ + lang: 'fr', + site_id, + name: channel.name + }) + }) + } + + return channels + } +} + +function parseToken(data) { + const [, token] = data.match(/"token":"([^"]+)/) || [null, null] + + return token +} + +function parseStart(item) { + return item && item.startTime ? dayjs(item.startTime) : null +} + +function parseIcon(info) { + return info ? info.URLImage : null +} + +function parseDescription(info) { + return info ? info.summary : null +} + +function parseInfo(data) { + if (!data || !data.detail || !data.detail.informations) return null + + return data.detail.informations +} + +async function loadProgramDetails(item) { + if (!item.onClick || !item.onClick.URLPage) return {} + + return await axios + .get(item.onClick.URLPage) + .then(r => r.data) + .catch(console.error) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.timeSlices)) return [] + + return data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) +} + +function parseCast(info, type) { + let people = [] + if (info && info.personnalities) { + const personnalities = info.personnalities.find(i => i.prefix == type) + if (!personnalities) return people + for (let person of personnalities.personnalitiesList) { + people.push(person.title) + } + } + return people +} + +function parseDate(info) { + return info && info.productionYear ? info.productionYear : null +} + +function parseRating(info) { + if (!info || !info.parentalRatings) return null + let rating = info.parentalRatings.find(i => i.authority === 'CSA') + if (!rating || Array.isArray(rating)) return null + if (rating.value === '1') return null + if (rating.value === '2') rating.value = '-10' + if (rating.value === '3') rating.value = '-12' + if (rating.value === '4') rating.value = '-16' + if (rating.value === '5') rating.value = '-18' + return { + system: rating.authority, + value: rating.value + } +} diff --git a/sites/canalplus.com/canalplus.com.test.js b/sites/canalplus.com/canalplus.com.test.js index 7b905e5b6..c3437908f 100644 --- a/sites/canalplus.com/canalplus.com.test.js +++ b/sites/canalplus.com/canalplus.com.test.js @@ -1,147 +1,147 @@ -// npm run channels:parse -- --config=./sites/canalplus.com/canalplus.com.config.js --output=./sites/canalplus.com/canalplus.com.channels.xml -// npm run grab -- --site=canalplus.com - -const { parser, url } = require('./canalplus.com.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) -jest.mock('axios') - -const channel = { - site_id: 'bi#198', - xmltv_id: 'CanalPlusCinemaFrance.fr' -} - -it('can generate valid url for today', done => { - axios.get.mockImplementation(url => { - if (url === 'https://www.canalplus.com/bi/programme-tv/') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const today = dayjs.utc().startOf('d') - url({ channel, date: today }) - .then(result => { - expect(result).toBe( - 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0' - ) - done() - }) - .catch(done) -}) - -it('can generate valid url for tomorrow', done => { - axios.get.mockImplementation(url => { - if (url === 'https://www.canalplus.com/bi/programme-tv/') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const tomorrow = dayjs.utc().startOf('d').add(1, 'd') - url({ channel, date: tomorrow }) - .then(result => { - expect(result).toBe( - 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1' - ) - done() - }) - .catch(done) -}) - -it('can parse response', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/6564630_50001.json?detailType=detailSeason&objectType=season&broadcastID=PLM_1196447642&episodeId=20482220_50001&brandID=4501558_50001&fromDiff=true' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if ( - url === - 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-01-12T06:28:00.000Z', - stop: '2023-01-12T12:06:00.000Z', - title: 'Le cercle', - description: - "Tant qu'il y aura du cinéma, LE CERCLE sera là. C'est la seule émission télévisée de débats critiques 100% consacrée au cinéma et elle rentre dans sa 18e saison. Chaque semaine, elle offre des joutes enflammées, joyeuses et sans condescendance, sur les films à l'affiche ; et invite avec \"Le questionnaire du CERCLE\" les réalisatrices et réalisateurs à venir partager leur passion cinéphile.", - icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573', - presenter: ['Lily Bloom'], - rating: { - system: 'CSA', - value: '-10' - } - }, - { - start: '2023-01-12T12:06:00.000Z', - stop: '2023-01-12T13:06:00.000Z', - title: 'Illusions perdues', - description: - "Pendant la Restauration, Lucien de Rubempré, jeune provincial d'Angoulême, se rêve poète. Il débarque à Paris en quête de gloire. Il a le soutien de Louise de Bargeton, une aristocrate qui croit en son talent. Pour gagner sa vie, Lucien trouve un emploi dans le journal dirigé par le peu scrupuleux Etienne Lousteau...", - icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485', - director: ['Xavier Giannoli'], - actors: [ - 'Benjamin Voisin', - 'Cécile de France', - 'Vincent Lacoste', - 'Xavier Dolan', - 'Gérard Depardieu', - 'Salomé Dewaels', - 'Jeanne Balibar', - 'Louis-Do de Lencquesaing', - 'Alexis Barbosa', - 'Jean-François Stévenin', - 'André Marcon', - 'Marie Cornillon' - ], - writer: ['Xavier Giannoli'], - rating: { - system: 'CSA', - value: '-10' - } - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = await parser({ content }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/canalplus.com/canalplus.com.config.js --output=./sites/canalplus.com/canalplus.com.channels.xml +// npm run grab -- --site=canalplus.com + +const { parser, url } = require('./canalplus.com.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) +jest.mock('axios') + +const channel = { + site_id: 'bi#198', + xmltv_id: 'CanalPlusCinemaFrance.fr' +} + +it('can generate valid url for today', done => { + axios.get.mockImplementation(url => { + if (url === 'https://www.canalplus.com/bi/programme-tv/') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const today = dayjs.utc().startOf('d') + url({ channel, date: today }) + .then(result => { + expect(result).toBe( + 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0' + ) + done() + }) + .catch(done) +}) + +it('can generate valid url for tomorrow', done => { + axios.get.mockImplementation(url => { + if (url === 'https://www.canalplus.com/bi/programme-tv/') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const tomorrow = dayjs.utc().startOf('d').add(1, 'd') + url({ channel, date: tomorrow }) + .then(result => { + expect(result).toBe( + 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1' + ) + done() + }) + .catch(done) +}) + +it('can parse response', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/6564630_50001.json?detailType=detailSeason&objectType=season&broadcastID=PLM_1196447642&episodeId=20482220_50001&brandID=4501558_50001&fromDiff=true' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if ( + url === + 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-01-12T06:28:00.000Z', + stop: '2023-01-12T12:06:00.000Z', + title: 'Le cercle', + description: + "Tant qu'il y aura du cinéma, LE CERCLE sera là. C'est la seule émission télévisée de débats critiques 100% consacrée au cinéma et elle rentre dans sa 18e saison. Chaque semaine, elle offre des joutes enflammées, joyeuses et sans condescendance, sur les films à l'affiche ; et invite avec \"Le questionnaire du CERCLE\" les réalisatrices et réalisateurs à venir partager leur passion cinéphile.", + icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573', + presenter: ['Lily Bloom'], + rating: { + system: 'CSA', + value: '-10' + } + }, + { + start: '2023-01-12T12:06:00.000Z', + stop: '2023-01-12T13:06:00.000Z', + title: 'Illusions perdues', + description: + "Pendant la Restauration, Lucien de Rubempré, jeune provincial d'Angoulême, se rêve poète. Il débarque à Paris en quête de gloire. Il a le soutien de Louise de Bargeton, une aristocrate qui croit en son talent. Pour gagner sa vie, Lucien trouve un emploi dans le journal dirigé par le peu scrupuleux Etienne Lousteau...", + icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485', + director: ['Xavier Giannoli'], + actors: [ + 'Benjamin Voisin', + 'Cécile de France', + 'Vincent Lacoste', + 'Xavier Dolan', + 'Gérard Depardieu', + 'Salomé Dewaels', + 'Jeanne Balibar', + 'Louis-Do de Lencquesaing', + 'Alexis Barbosa', + 'Jean-François Stévenin', + 'André Marcon', + 'Marie Cornillon' + ], + writer: ['Xavier Giannoli'], + rating: { + system: 'CSA', + value: '-10' + } + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = await parser({ content }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cgates.lt/cgates.lt.config.js b/sites/cgates.lt/cgates.lt.config.js index efdc3ca29..0be507dff 100644 --- a/sites/cgates.lt/cgates.lt.config.js +++ b/sites/cgates.lt/cgates.lt.config.js @@ -1,92 +1,92 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'cgates.lt', - days: 2, - url: function ({ channel }) { - return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let html = await axios - .get('https://www.cgates.lt/televizija/tv-programa-savaitei/') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - const items = $('.kanalas_wrap').toArray() - - return items.map(item => { - const name = $(item).find('h6').text().trim() - const link = $(item).find('a').attr('href') - const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null] - - return { - lang: 'lt', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim() - - return title || $item('td:nth-child(2)').text().trim() -} - -function parseDescription($item) { - return $item('.vc_toggle_content > p').text().trim() -} - -function parseStart($item, date) { - const time = $item('.laikas') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius') -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const section = $( - 'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div' - ) - .filter(function () { - return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1 - }) - .first() - - return $('.tv_programa tr', section).toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'cgates.lt', + days: 2, + url: function ({ channel }) { + return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let html = await axios + .get('https://www.cgates.lt/televizija/tv-programa-savaitei/') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + const items = $('.kanalas_wrap').toArray() + + return items.map(item => { + const name = $(item).find('h6').text().trim() + const link = $(item).find('a').attr('href') + const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null] + + return { + lang: 'lt', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim() + + return title || $item('td:nth-child(2)').text().trim() +} + +function parseDescription($item) { + return $item('.vc_toggle_content > p').text().trim() +} + +function parseStart($item, date) { + const time = $item('.laikas') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius') +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const section = $( + 'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div' + ) + .filter(function () { + return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1 + }) + .first() + + return $('.tv_programa tr', section).toArray() +} diff --git a/sites/cgates.lt/cgates.lt.test.js b/sites/cgates.lt/cgates.lt.test.js index ac1abbecc..f202b3047 100644 --- a/sites/cgates.lt/cgates.lt.test.js +++ b/sites/cgates.lt/cgates.lt.test.js @@ -1,52 +1,52 @@ -// npm run channels:parse -- --config=./sites/cgates.lt/cgates.lt.config.js --output=./sites/cgates.lt/cgates.lt.channels.xml -// npm run grab -- --site=cgates.lt - -const { parser, url } = require('./cgates.lt.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lrt-televizija-hd', - xmltv_id: 'LRTTV.lt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(35) - expect(results[0]).toMatchObject({ - start: '2022-08-29T21:05:00.000Z', - stop: '2022-08-29T21:30:00.000Z', - title: '31-oji nuovada (District 31), Drama, 2016', - description: - 'Seriale pasakojama apie kasdienius policijos išbandymus ir sunkumus. Vadovybė pertvarko Monrealio miesto policijos struktūrą: išskirsto į 36 policijos nuovadas, kad šios būtų arčiau gyventojų. 31-osios nuovados darbuotojams tenka kone sunkiausias darbas: šiame miesto rajone gyvena socialiai remtinos šeimos, nuolat kovojančios su turtingųjų klase, įsipliekia ir rasinių konfliktų. Be to, čia akivaizdus kartų atotrūkis, o tapti nusikalstamo pasaulio dalimi labai lengva. Serialo siužetas – intensyvus, nauji nusikaltimai tiriami kiekvieną savaitę. Čia vaizduojamas nepagražintas nusikalstamas pasaulis, jo poveikis rajono gyventojams. Policijos nuovados darbuotojai narplios įvairiausių nusikaltimų schemas. Tai ir pagrobimai, įsilaužimai, žmogžudystės, smurtas artimoje aplinkoje, lytiniai nusikaltimai, prekyba narkotikais, teroristinių išpuolių grėsmė ir pan. Šis serialas leis žiūrovui įsigilinti į policijos pareigūnų realybę, pateiks skirtingą požiūrį į kiekvieną nusikaltimą.' - }) - - expect(results[34]).toMatchObject({ - start: '2022-08-30T20:45:00.000Z', - stop: '2022-08-30T21:15:00.000Z', - title: '31-oji nuovada (District 31), Drama, 2016!' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/cgates.lt/cgates.lt.config.js --output=./sites/cgates.lt/cgates.lt.channels.xml +// npm run grab -- --site=cgates.lt + +const { parser, url } = require('./cgates.lt.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lrt-televizija-hd', + xmltv_id: 'LRTTV.lt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(35) + expect(results[0]).toMatchObject({ + start: '2022-08-29T21:05:00.000Z', + stop: '2022-08-29T21:30:00.000Z', + title: '31-oji nuovada (District 31), Drama, 2016', + description: + 'Seriale pasakojama apie kasdienius policijos išbandymus ir sunkumus. Vadovybė pertvarko Monrealio miesto policijos struktūrą: išskirsto į 36 policijos nuovadas, kad šios būtų arčiau gyventojų. 31-osios nuovados darbuotojams tenka kone sunkiausias darbas: šiame miesto rajone gyvena socialiai remtinos šeimos, nuolat kovojančios su turtingųjų klase, įsipliekia ir rasinių konfliktų. Be to, čia akivaizdus kartų atotrūkis, o tapti nusikalstamo pasaulio dalimi labai lengva. Serialo siužetas – intensyvus, nauji nusikaltimai tiriami kiekvieną savaitę. Čia vaizduojamas nepagražintas nusikalstamas pasaulis, jo poveikis rajono gyventojams. Policijos nuovados darbuotojai narplios įvairiausių nusikaltimų schemas. Tai ir pagrobimai, įsilaužimai, žmogžudystės, smurtas artimoje aplinkoje, lytiniai nusikaltimai, prekyba narkotikais, teroristinių išpuolių grėsmė ir pan. Šis serialas leis žiūrovui įsigilinti į policijos pareigūnų realybę, pateiks skirtingą požiūrį į kiekvieną nusikaltimą.' + }) + + expect(results[34]).toMatchObject({ + start: '2022-08-30T20:45:00.000Z', + stop: '2022-08-30T21:15:00.000Z', + title: '31-oji nuovada (District 31), Drama, 2016!' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js index 7f8c46a2a..66ae219c1 100644 --- a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js +++ b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js @@ -1,47 +1,47 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'chaines-tv.orange.fr', - days: 2, - url({ channel, date }) { - return `https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=${date.valueOf()},${date - .add(1, 'd') - .valueOf()}&after=${channel.site_id}&limit=1` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item, start) - programs.push({ - title: item.title, - category: item.genreDetailed, - description: item.synopsis, - icon: parseIcon(item), - start: start.toJSON(), - stop: stop.toJSON() - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.covers && item.covers.length ? item.covers[0].url : null -} - -function parseStart(item) { - return dayjs.unix(item.diffusionDate) -} - -function parseStop(item, start) { - return start.add(item.duration, 's') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - - return data && data[channel.site_id] ? data[channel.site_id] : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'chaines-tv.orange.fr', + days: 2, + url({ channel, date }) { + return `https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=${date.valueOf()},${date + .add(1, 'd') + .valueOf()}&after=${channel.site_id}&limit=1` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item, start) + programs.push({ + title: item.title, + category: item.genreDetailed, + description: item.synopsis, + icon: parseIcon(item), + start: start.toJSON(), + stop: stop.toJSON() + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.covers && item.covers.length ? item.covers[0].url : null +} + +function parseStart(item) { + return dayjs.unix(item.diffusionDate) +} + +function parseStop(item, start) { + return start.add(item.duration, 's') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + + return data && data[channel.site_id] ? data[channel.site_id] : [] +} diff --git a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js index ff1ec4318..9e2fc529a 100644 --- a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js +++ b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js @@ -1,48 +1,48 @@ -// npm run grab -- --site=chaines-tv.orange.fr - -const { parser, url } = require('./chaines-tv.orange.fr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '192', - xmltv_id: 'TF1.fr' -} -const content = - '{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d\'un de ses vieux amis.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]}' - -it('can generate valid url', () => { - const result = url({ channel, date }) - expect(result).toBe( - 'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-07T23:35:00.000Z', - stop: '2021-11-08T00:20:00.000Z', - title: 'Tête de liste', - description: - "Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.", - category: 'Série Suspense', - icon: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=chaines-tv.orange.fr + +const { parser, url } = require('./chaines-tv.orange.fr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '192', + xmltv_id: 'TF1.fr' +} +const content = + '{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d\'un de ses vieux amis.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]}' + +it('can generate valid url', () => { + const result = url({ channel, date }) + expect(result).toBe( + 'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-07T23:35:00.000Z', + stop: '2021-11-08T00:20:00.000Z', + title: 'Tête de liste', + description: + "Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.", + category: 'Série Suspense', + icon: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/clickthecity.com/clickthecity.com.config.js b/sites/clickthecity.com/clickthecity.com.config.js index 917495eab..7855a546d 100644 --- a/sites/clickthecity.com/clickthecity.com.config.js +++ b/sites/clickthecity.com/clickthecity.com.config.js @@ -1,99 +1,99 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'clickthecity.com', - days: 2, - url({ channel }) { - return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}` - }, - request: { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data({ date }) { - const params = new URLSearchParams() - params.append( - 'optDate', - DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd') - ) - params.append('optTime', '00:00:00') - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - let stop = parseStop($item, date) - if (!start || !stop) return - if (start > stop) { - stop = stop.plus({ days: 1 }) - } - - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.clickthecity.com/tv/channels/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('#channels .col').toArray() - - return items.map(item => { - const name = $(item).find('.card-body').text().trim() - const url = $(item).find('a').attr('href') - const [, site_id] = url.match(/netid=(\d+)/) || [null, null] - - return { - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('td > a').text().trim() -} - -function parseStart($item, date) { - const url = $item('td.cPrg > a').attr('href') || '' - let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() -} - -function parseStop($item, date) { - const url = $item('td.cPrg > a').attr('href') || '' - let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#tvlistings > tbody > tr') - .filter(function () { - return $(this).find('td.cPrg').length - }) - .toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'clickthecity.com', + days: 2, + url({ channel }) { + return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}` + }, + request: { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + data({ date }) { + const params = new URLSearchParams() + params.append( + 'optDate', + DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd') + ) + params.append('optTime', '00:00:00') + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + let stop = parseStop($item, date) + if (!start || !stop) return + if (start > stop) { + stop = stop.plus({ days: 1 }) + } + + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.clickthecity.com/tv/channels/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('#channels .col').toArray() + + return items.map(item => { + const name = $(item).find('.card-body').text().trim() + const url = $(item).find('a').attr('href') + const [, site_id] = url.match(/netid=(\d+)/) || [null, null] + + return { + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('td > a').text().trim() +} + +function parseStart($item, date) { + const url = $item('td.cPrg > a').attr('href') || '' + let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() +} + +function parseStop($item, date) { + const url = $item('td.cPrg > a').attr('href') || '' + let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#tvlistings > tbody > tr') + .filter(function () { + return $(this).find('td.cPrg').length + }) + .toArray() +} diff --git a/sites/clickthecity.com/clickthecity.com.test.js b/sites/clickthecity.com/clickthecity.com.test.js index 33ae1debb..46ae823b4 100644 --- a/sites/clickthecity.com/clickthecity.com.test.js +++ b/sites/clickthecity.com/clickthecity.com.test.js @@ -1,70 +1,70 @@ -// npm run channels:parse -- --config=./sites/clickthecity.com/clickthecity.com.config.js --output=./sites/clickthecity.com/clickthecity.com.channels.xml -// npm run grab -- --site=clickthecity.com - -const { parser, url, request } = require('./clickthecity.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5', - xmltv_id: 'TV5.ph' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'content-type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date }) - expect(result.get('optDate')).toBe('2023-06-12') - expect(result.get('optTime')).toBe('00:00:00') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(20) - - expect(results[0]).toMatchObject({ - start: '2023-06-11T21:00:00.000Z', - stop: '2023-06-11T22:00:00.000Z', - title: 'Word Of God' - }) - - expect(results[19]).toMatchObject({ - start: '2023-06-12T15:30:00.000Z', - stop: '2023-06-12T16:00:00.000Z', - title: 'La Suerte De Loli' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/clickthecity.com/clickthecity.com.config.js --output=./sites/clickthecity.com/clickthecity.com.channels.xml +// npm run grab -- --site=clickthecity.com + +const { parser, url, request } = require('./clickthecity.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5', + xmltv_id: 'TV5.ph' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'content-type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date }) + expect(result.get('optDate')).toBe('2023-06-12') + expect(result.get('optTime')).toBe('00:00:00') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(20) + + expect(results[0]).toMatchObject({ + start: '2023-06-11T21:00:00.000Z', + stop: '2023-06-11T22:00:00.000Z', + title: 'Word Of God' + }) + + expect(results[19]).toMatchObject({ + start: '2023-06-12T15:30:00.000Z', + stop: '2023-06-12T16:00:00.000Z', + title: 'La Suerte De Loli' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/compulms.com/compulms.com.config.js b/sites/compulms.com/compulms.com.config.js index c6202a487..3304a8786 100644 --- a/sites/compulms.com/compulms.com.config.js +++ b/sites/compulms.com/compulms.com.config.js @@ -1,33 +1,33 @@ -const parser = require('epg-parser') - -module.exports = { - site: 'compulms.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml', - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.title?.[0].value, - description: item.desc?.[0].value, - icon: item.icon?.[0], - start: item.start, - stop: item.stop - }) - }) - - return programs - } -} - -function parseItems(content, channel, date) { - const { programs } = parser.parse(content) - - return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) -} +const parser = require('epg-parser') + +module.exports = { + site: 'compulms.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml', + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.title?.[0].value, + description: item.desc?.[0].value, + icon: item.icon?.[0], + start: item.start, + stop: item.stop + }) + }) + + return programs + } +} + +function parseItems(content, channel, date) { + const { programs } = parser.parse(content) + + return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) +} diff --git a/sites/compulms.com/compulms.com.test.js b/sites/compulms.com/compulms.com.test.js index 92c1fae57..4f63fde89 100644 --- a/sites/compulms.com/compulms.com.test.js +++ b/sites/compulms.com/compulms.com.test.js @@ -1,39 +1,39 @@ -// npm run grab -- --site=compulms.com - -const { parser, url } = require('./compulms.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'EnerGeek Retro', - xmltv_id: 'EnerGeekRetro.cl' -} - -it('can generate valid url', () => { - expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - let results = parser({ content, channel, date }) - - expect(results[0]).toMatchObject({ - start: '2022-11-29T03:00:00.000Z', - stop: '2022-11-29T03:30:00.000Z', - title: 'Noir', - description: - 'Kirika Yuumura es una adolescente japonesa que no recuerda nada de su pasado, salvo la palabra NOIR, por lo que decidirá contactar con Mireille Bouquet, una asesina profesional para que la ayude a investigar. Ambas forman un equipo muy eficiente, que resuelve un trabajo tras otro con gran éxito, hasta que aparece un grupo conocido como "Les Soldats", relacionados con el pasado de Kirika. Estos tratarán de eliminar a las dos chicas, antes de que indaguen más hondo sobre la verdad acerca de Noir', - icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=compulms.com + +const { parser, url } = require('./compulms.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'EnerGeek Retro', + xmltv_id: 'EnerGeekRetro.cl' +} + +it('can generate valid url', () => { + expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + let results = parser({ content, channel, date }) + + expect(results[0]).toMatchObject({ + start: '2022-11-29T03:00:00.000Z', + stop: '2022-11-29T03:30:00.000Z', + title: 'Noir', + description: + 'Kirika Yuumura es una adolescente japonesa que no recuerda nada de su pasado, salvo la palabra NOIR, por lo que decidirá contactar con Mireille Bouquet, una asesina profesional para que la ayude a investigar. Ambas forman un equipo muy eficiente, que resuelve un trabajo tras otro con gran éxito, hasta que aparece un grupo conocido como "Les Soldats", relacionados con el pasado de Kirika. Estos tratarán de eliminar a las dos chicas, antes de que indaguen más hondo sobre la verdad acerca de Noir', + icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/comteco.com.bo/comteco.com.bo.config.js b/sites/comteco.com.bo/comteco.com.bo.config.js index 04fbc5c89..2fbde814e 100644 --- a/sites/comteco.com.bo/comteco.com.bo.config.js +++ b/sites/comteco.com.bo/comteco.com.bo.config.js @@ -1,68 +1,68 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'comteco.com.bo', - days: 2, - url: function ({ channel }) { - return `https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/${channel.site_id}` - }, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: function ({ date }) { - const params = new URLSearchParams() - params.append('_method', 'POST') - params.append('fechaini', date.format('D/M/YYYY')) - params.append('fechafin', date.format('D/M/YYYY')) - - return params - } - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ title: parseTitle($item), start, stop }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('div > div.col-xs-11 > p > span').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm:ss', 'America/La_Paz') -} - -function parseTitle($item) { - return $item('div > div.col-xs-11 > p > strong').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#datosasociados > div > .list-group-item').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'comteco.com.bo', + days: 2, + url: function ({ channel }) { + return `https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/${channel.site_id}` + }, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: function ({ date }) { + const params = new URLSearchParams() + params.append('_method', 'POST') + params.append('fechaini', date.format('D/M/YYYY')) + params.append('fechafin', date.format('D/M/YYYY')) + + return params + } + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ title: parseTitle($item), start, stop }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('div > div.col-xs-11 > p > span').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm:ss', 'America/La_Paz') +} + +function parseTitle($item) { + return $item('div > div.col-xs-11 > p > strong').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#datosasociados > div > .list-group-item').toArray() +} diff --git a/sites/comteco.com.bo/comteco.com.bo.test.js b/sites/comteco.com.bo/comteco.com.bo.test.js index c0514bf4f..bdf7cfe02 100644 --- a/sites/comteco.com.bo/comteco.com.bo.test.js +++ b/sites/comteco.com.bo/comteco.com.bo.test.js @@ -1,74 +1,74 @@ -// npm run grab -- --site=comteco.com.bo - -const { parser, url, request } = require('./comteco.com.bo.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ABYA YALA', - xmltv_id: 'AbyaYalaTV.bo' -} -const content = - '

    Canal Analógico: 48

    ABYA YALA

    00:00:00 Abya Yala noticias - 3ra edición

    01:00:00 Cierre de emisión

    23:00:00 Referentes

    Regresar a canales

    ' - -it('can generate valid url', () => { - expect(url({ channel })).toBe( - 'https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/ABYA YALA' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date }) - expect(result.get('_method')).toBe('POST') - expect(result.get('fechaini')).toBe('25/11/2021') - expect(result.get('fechafin')).toBe('25/11/2021') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-25T04:00:00.000Z', - stop: '2021-11-25T05:00:00.000Z', - title: 'Abya Yala noticias - 3ra edición' - }, - { - start: '2021-11-25T05:00:00.000Z', - stop: '2021-11-26T03:00:00.000Z', - title: 'Cierre de emisión' - }, - { - start: '2021-11-26T03:00:00.000Z', - stop: '2021-11-26T03:30:00.000Z', - title: 'Referentes' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=comteco.com.bo + +const { parser, url, request } = require('./comteco.com.bo.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ABYA YALA', + xmltv_id: 'AbyaYalaTV.bo' +} +const content = + '

    Canal Analógico: 48

    ABYA YALA

    00:00:00 Abya Yala noticias - 3ra edición

    01:00:00 Cierre de emisión

    23:00:00 Referentes

    Regresar a canales

    ' + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/ABYA YALA' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date }) + expect(result.get('_method')).toBe('POST') + expect(result.get('fechaini')).toBe('25/11/2021') + expect(result.get('fechafin')).toBe('25/11/2021') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-25T04:00:00.000Z', + stop: '2021-11-25T05:00:00.000Z', + title: 'Abya Yala noticias - 3ra edición' + }, + { + start: '2021-11-25T05:00:00.000Z', + stop: '2021-11-26T03:00:00.000Z', + title: 'Cierre de emisión' + }, + { + start: '2021-11-26T03:00:00.000Z', + stop: '2021-11-26T03:30:00.000Z', + title: 'Referentes' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cosmote.gr/cosmote.gr.config.js b/sites/cosmote.gr/cosmote.gr.config.js index 29ce29c4a..d5fcc97c8 100644 --- a/sites/cosmote.gr/cosmote.gr.config.js +++ b/sites/cosmote.gr/cosmote.gr.config.js @@ -1,78 +1,78 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'cosmote.gr', - days: 2, - url: function ({ date, channel }) { - return `https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=${date.format( - 'DD-MM-YYYY' - )}&_channelprogram_WAR_OTETVportlet_articleTitleUrl=${channel.site_id}` - }, - parser: function ({ date, content }) { - let programs = [] - const items = parseItems(content) - items.forEach((item, i) => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (i === 0 && start.hour > 12 && start.hour < 21) { - date = date.subtract(1, 'd') - start = start.minus({ days: 1 }) - } - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.channel_program-table--program > a').text() -} - -function parseCategory($item) { - const typeString = $item('.channel_program-table--program_type') - .children() - .remove() - .end() - .text() - .trim() - const [, category] = typeString.match(/\| (.*)/) || [null, null] - - return category -} - -function parseStart($item, date) { - const timeString = $item('span.start-time').text() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC() -} - -function parseStop($item, date) { - const timeString = $item('span.end-time').text() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#_channelprogram_WAR_OTETVportlet_programs > tr.d-sm-table-row').toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'cosmote.gr', + days: 2, + url: function ({ date, channel }) { + return `https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=${date.format( + 'DD-MM-YYYY' + )}&_channelprogram_WAR_OTETVportlet_articleTitleUrl=${channel.site_id}` + }, + parser: function ({ date, content }) { + let programs = [] + const items = parseItems(content) + items.forEach((item, i) => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (i === 0 && start.hour > 12 && start.hour < 21) { + date = date.subtract(1, 'd') + start = start.minus({ days: 1 }) + } + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.channel_program-table--program > a').text() +} + +function parseCategory($item) { + const typeString = $item('.channel_program-table--program_type') + .children() + .remove() + .end() + .text() + .trim() + const [, category] = typeString.match(/\| (.*)/) || [null, null] + + return category +} + +function parseStart($item, date) { + const timeString = $item('span.start-time').text() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC() +} + +function parseStop($item, date) { + const timeString = $item('span.end-time').text() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#_channelprogram_WAR_OTETVportlet_programs > tr.d-sm-table-row').toArray() +} diff --git a/sites/cosmote.gr/cosmote.gr.test.js b/sites/cosmote.gr/cosmote.gr.test.js index 5b8186508..ab8e215f8 100644 --- a/sites/cosmote.gr/cosmote.gr.test.js +++ b/sites/cosmote.gr/cosmote.gr.test.js @@ -1,79 +1,79 @@ -// npm run grab -- --site=cosmote.gr - -const { parser, url } = require('./cosmote.gr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const date = dayjs.utc('2023-06-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '4e', - xmltv_id: '4E.gr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=08-06-2023&_channelprogram_WAR_OTETVportlet_articleTitleUrl=4e' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content1.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-07T20:30:00.000Z', - stop: '2023-06-07T21:45:00.000Z', - title: 'Τηλεφημερίδα', - category: 'Εκπομπή - Μαγκαζίνο' - }) - - expect(results[30]).toMatchObject({ - start: '2023-06-08T19:45:00.000Z', - stop: '2023-06-08T20:30:00.000Z', - title: 'Μικρό Απόδειπνο', - category: 'Special' - }) -}) - -it('can parse response when the guide starting before midnight', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content2.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-07T21:30:00.000Z', - stop: '2023-06-07T22:30:00.000Z', - title: 'Καλύτερα Αργά', - category: 'Ψυχαγωγική Εκπομπή' - }) - - expect(results[22]).toMatchObject({ - start: '2023-06-08T19:00:00.000Z', - stop: '2023-06-08T21:30:00.000Z', - title: 'Πίσω Από Τις Γραμμές', - category: 'Εκπομπή - Μαγκαζίνο' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=cosmote.gr + +const { parser, url } = require('./cosmote.gr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const date = dayjs.utc('2023-06-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '4e', + xmltv_id: '4E.gr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=08-06-2023&_channelprogram_WAR_OTETVportlet_articleTitleUrl=4e' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content1.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-07T20:30:00.000Z', + stop: '2023-06-07T21:45:00.000Z', + title: 'Τηλεφημερίδα', + category: 'Εκπομπή - Μαγκαζίνο' + }) + + expect(results[30]).toMatchObject({ + start: '2023-06-08T19:45:00.000Z', + stop: '2023-06-08T20:30:00.000Z', + title: 'Μικρό Απόδειπνο', + category: 'Special' + }) +}) + +it('can parse response when the guide starting before midnight', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content2.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-07T21:30:00.000Z', + stop: '2023-06-07T22:30:00.000Z', + title: 'Καλύτερα Αργά', + category: 'Ψυχαγωγική Εκπομπή' + }) + + expect(results[22]).toMatchObject({ + start: '2023-06-08T19:00:00.000Z', + stop: '2023-06-08T21:30:00.000Z', + title: 'Πίσω Από Τις Γραμμές', + category: 'Εκπομπή - Μαγκαζίνο' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/delta.nl/delta.nl.config.js b/sites/delta.nl/delta.nl.config.js index f37c26a98..0a9ebd286 100644 --- a/sites/delta.nl/delta.nl.config.js +++ b/sites/delta.nl/delta.nl.config.js @@ -1,70 +1,70 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'delta.nl', - days: 2, - url: function ({ channel, date }) { - return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date - .add(1, 'd') - .unix()}&includeDetails=true&channels=${channel.site_id}` - }, - async parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const details = await loadProgramDetails(item) - programs.push({ - title: item.title, - icon: item.images.thumbnail.url, - description: details.description, - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - } - - return programs - }, - async channels() { - const items = await axios - .get('https://clientapi.tv.delta.nl/channels/list') - .then(r => r.data) - .catch(console.log) - - return items - .filter(i => i.type === 'TV') - .map(item => { - return { - lang: 'nl', - site_id: item['ID'], - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.ID) return {} - const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.start) -} - -function parseStop(item) { - return dayjs.unix(item.end) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - - return data[channel.site_id] || [] -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'delta.nl', + days: 2, + url: function ({ channel, date }) { + return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date + .add(1, 'd') + .unix()}&includeDetails=true&channels=${channel.site_id}` + }, + async parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const details = await loadProgramDetails(item) + programs.push({ + title: item.title, + icon: item.images.thumbnail.url, + description: details.description, + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + } + + return programs + }, + async channels() { + const items = await axios + .get('https://clientapi.tv.delta.nl/channels/list') + .then(r => r.data) + .catch(console.log) + + return items + .filter(i => i.type === 'TV') + .map(item => { + return { + lang: 'nl', + site_id: item['ID'], + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.ID) return {} + const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.start) +} + +function parseStop(item) { + return dayjs.unix(item.end) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + + return data[channel.site_id] || [] +} diff --git a/sites/delta.nl/delta.nl.test.js b/sites/delta.nl/delta.nl.test.js index c4a49af91..e7467d249 100644 --- a/sites/delta.nl/delta.nl.test.js +++ b/sites/delta.nl/delta.nl.test.js @@ -1,70 +1,70 @@ -// npm run channels:parse -- --config=./sites/delta.nl/delta.nl.config.js --output=./sites/delta.nl/delta.nl.channels.xml -// npm run grab -- --site=delta.nl - -const { parser, url } = require('./delta.nl.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2021-11-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1', - xmltv_id: 'NPO1.nl' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1' - ) -}) - -it('can parse response', done => { - axios.get.mockImplementation(() => - Promise.resolve({ - data: JSON.parse( - '{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"Eigen Huis & Tuin: Lekker Leven","description":"Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/b291/561946.jpg"}},"additionalInformation":{"metadataID":"M~c512c206-95e5-11ec-87d8-494f70130311","externalMetadataID":"E~RTL4-89d99356_6599_4b65_a7a0_a93f39019645"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}' - ) - }) - ) - - const content = - '{"1":[{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"NOS Journaal","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg"}},"additionalInformation":{"metadataID":"M~944f3c6e-3d19-11ec-9faf-2735f2e98d2a","externalMetadataID":"E~TV01-2026117420668"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}]}' - - parser({ date, channel, content }) - .then(result => { - expect(result).toMatchObject([ - { - start: '2021-11-11T23:56:00.000Z', - stop: '2021-11-12T00:22:00.000Z', - title: 'NOS Journaal', - description: - 'Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.', - icon: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg' - } - ]) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: '{"code":500,"message":"Error retrieving guide"}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(error => { - done(error) - }) -}) +// npm run channels:parse -- --config=./sites/delta.nl/delta.nl.config.js --output=./sites/delta.nl/delta.nl.channels.xml +// npm run grab -- --site=delta.nl + +const { parser, url } = require('./delta.nl.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2021-11-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1', + xmltv_id: 'NPO1.nl' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1' + ) +}) + +it('can parse response', done => { + axios.get.mockImplementation(() => + Promise.resolve({ + data: JSON.parse( + '{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"Eigen Huis & Tuin: Lekker Leven","description":"Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/b291/561946.jpg"}},"additionalInformation":{"metadataID":"M~c512c206-95e5-11ec-87d8-494f70130311","externalMetadataID":"E~RTL4-89d99356_6599_4b65_a7a0_a93f39019645"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}' + ) + }) + ) + + const content = + '{"1":[{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"NOS Journaal","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg"}},"additionalInformation":{"metadataID":"M~944f3c6e-3d19-11ec-9faf-2735f2e98d2a","externalMetadataID":"E~TV01-2026117420668"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}]}' + + parser({ date, channel, content }) + .then(result => { + expect(result).toMatchObject([ + { + start: '2021-11-11T23:56:00.000Z', + stop: '2021-11-12T00:22:00.000Z', + title: 'NOS Journaal', + description: + 'Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.', + icon: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg' + } + ]) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: '{"code":500,"message":"Error retrieving guide"}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(error => { + done(error) + }) +}) diff --git a/sites/digiturk.com.tr/digiturk.com.tr.config.js b/sites/digiturk.com.tr/digiturk.com.tr.config.js index 670fb569e..694cbd91c 100644 --- a/sites/digiturk.com.tr/digiturk.com.tr.config.js +++ b/sites/digiturk.com.tr/digiturk.com.tr.config.js @@ -1,77 +1,77 @@ -const _ = require('lodash') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) -// category list is not complete -// const categories = { -// '00': 'Diğer', -// E0: 'Romantik Komedi', -// E1: 'Aksiyon', -// E4: 'Macera', -// E5: 'Dram', -// E6: 'Fantastik', -// E7: 'Komedi', -// E8: 'Korku', -// EB: 'Polisiye', -// EF: 'Western', -// FA: 'Macera', -// FB: 'Yarışma', -// FC: 'Eğlence', -// F0: 'Reality-Show', -// F2: 'Haberler', -// F4: 'Belgesel', -// F6: 'Eğitim', -// F7: 'Sanat ve Kültür', -// F9: 'Life Style' -// } - -module.exports = { - site: 'digiturk.com.tr', - days: 2, - url: function ({ date, channel }) { - return `https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=${ - channel.site_id - }&date=${date.format('DD.MM.YYYY')}&tomorrow=false&primetime=false` - }, - request: { - method: 'GET', - headers: { - Referer: 'https://www.digiturk.com.tr/' - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.PName, - // description: item.LongDescription, - // category: parseCategory(item), - start: parseTime(item.PStartTime), - stop: parseTime(item.PEndTime) - }) - }) - - programs = _.sortBy(programs, 'start') - - return programs - } -} - -function parseTime(time) { - let timestamp = parseInt(time.replace('/Date(', '').replace('+0300)/', '')) - return dayjs(timestamp) -} - -// function parseCategory(item) { -// return (item.PGenre) ? categories[item.PGenre] : null -// } - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - return data && data.BChannels && data.BChannels[0].CPrograms ? data.BChannels[0].CPrograms : [] -} +const _ = require('lodash') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) +// category list is not complete +// const categories = { +// '00': 'Diğer', +// E0: 'Romantik Komedi', +// E1: 'Aksiyon', +// E4: 'Macera', +// E5: 'Dram', +// E6: 'Fantastik', +// E7: 'Komedi', +// E8: 'Korku', +// EB: 'Polisiye', +// EF: 'Western', +// FA: 'Macera', +// FB: 'Yarışma', +// FC: 'Eğlence', +// F0: 'Reality-Show', +// F2: 'Haberler', +// F4: 'Belgesel', +// F6: 'Eğitim', +// F7: 'Sanat ve Kültür', +// F9: 'Life Style' +// } + +module.exports = { + site: 'digiturk.com.tr', + days: 2, + url: function ({ date, channel }) { + return `https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=${ + channel.site_id + }&date=${date.format('DD.MM.YYYY')}&tomorrow=false&primetime=false` + }, + request: { + method: 'GET', + headers: { + Referer: 'https://www.digiturk.com.tr/' + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.PName, + // description: item.LongDescription, + // category: parseCategory(item), + start: parseTime(item.PStartTime), + stop: parseTime(item.PEndTime) + }) + }) + + programs = _.sortBy(programs, 'start') + + return programs + } +} + +function parseTime(time) { + let timestamp = parseInt(time.replace('/Date(', '').replace('+0300)/', '')) + return dayjs(timestamp) +} + +// function parseCategory(item) { +// return (item.PGenre) ? categories[item.PGenre] : null +// } + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + return data && data.BChannels && data.BChannels[0].CPrograms ? data.BChannels[0].CPrograms : [] +} diff --git a/sites/digiturk.com.tr/digiturk.com.tr.test.js b/sites/digiturk.com.tr/digiturk.com.tr.test.js index 61f6c06f3..3e24acabf 100644 --- a/sites/digiturk.com.tr/digiturk.com.tr.test.js +++ b/sites/digiturk.com.tr/digiturk.com.tr.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=digiturk.com.tr - -const { parser, url } = require('./digiturk.com.tr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '14', - xmltv_id: 'beINMovies2Action.qa' -} - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=14&date=19.01.2023&tomorrow=false&primetime=false' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-18T20:40:00.000Z', - stop: '2023-01-18T22:32:00.000Z', - title: 'PARÇALANMIŞ' - }) - - expect(results[10]).toMatchObject({ - start: '2023-01-19T05:04:00.000Z', - stop: '2023-01-19T06:42:00.000Z', - title: 'HIZLI VE ÖFKELİ: TOKYO YARIŞI' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '' }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=digiturk.com.tr + +const { parser, url } = require('./digiturk.com.tr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '14', + xmltv_id: 'beINMovies2Action.qa' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=14&date=19.01.2023&tomorrow=false&primetime=false' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-18T20:40:00.000Z', + stop: '2023-01-18T22:32:00.000Z', + title: 'PARÇALANMIŞ' + }) + + expect(results[10]).toMatchObject({ + start: '2023-01-19T05:04:00.000Z', + stop: '2023-01-19T06:42:00.000Z', + title: 'HIZLI VE ÖFKELİ: TOKYO YARIŞI' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com.ar/directv.com.ar.config.js b/sites/directv.com.ar/directv.com.ar.config.js index 9918e2911..a483cbb29 100644 --- a/sites/directv.com.ar/directv.com.ar.config.js +++ b/sites/directv.com.ar/directv.com.ar.config.js @@ -1,100 +1,100 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'directv.com.ar', - days: 2, - url: 'https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming', - request: { - method: 'POST', - headers: { - Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;', - Accept: '*/*', - 'Accept-Language': 'es-419,es;q=0.9', - Connection: 'keep-alive', - 'Content-Type': 'application/json; charset=UTF-8', - Origin: 'https://www.directv.com.ar', - Referer: 'https://www.directv.com.ar/guia/ChannelDetail.aspx?id=1740&name=TLCHD', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', - 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', - 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': '"Windows"' - }, - data({ channel, date }) { - const [channelNum, channelName] = channel.site_id.split('#') - - return { - filterParameters: { - day: date.date(), - time: 0, - minute: 0, - month: date.month() + 1, - year: date.year(), - offSetValue: 0, - homeScreenFilter: '', - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum, - channelName: channelName.replace('&', '&') - } - } - } - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - rating: parseRating(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} - -function parseStart(item) { - return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') -} - -function parseStop(item) { - return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') -} - -function parseItems(content, channel) { - if (!content) return [] - let [ChannelNumber, ChannelName] = channel.site_id.split('#') - ChannelName = ChannelName.replace('&', '&') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.d)) return [] - const channelData = data.d.find( - c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName - ) - - return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'directv.com.ar', + days: 2, + url: 'https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming', + request: { + method: 'POST', + headers: { + Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;', + Accept: '*/*', + 'Accept-Language': 'es-419,es;q=0.9', + Connection: 'keep-alive', + 'Content-Type': 'application/json; charset=UTF-8', + Origin: 'https://www.directv.com.ar', + Referer: 'https://www.directv.com.ar/guia/ChannelDetail.aspx?id=1740&name=TLCHD', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"' + }, + data({ channel, date }) { + const [channelNum, channelName] = channel.site_id.split('#') + + return { + filterParameters: { + day: date.date(), + time: 0, + minute: 0, + month: date.month() + 1, + year: date.year(), + offSetValue: 0, + homeScreenFilter: '', + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum, + channelName: channelName.replace('&', '&') + } + } + } + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + rating: parseRating(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} + +function parseStart(item) { + return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') +} + +function parseStop(item) { + return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') +} + +function parseItems(content, channel) { + if (!content) return [] + let [ChannelNumber, ChannelName] = channel.site_id.split('#') + ChannelName = ChannelName.replace('&', '&') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.d)) return [] + const channelData = data.d.find( + c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName + ) + + return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] +} diff --git a/sites/directv.com.ar/directv.com.ar.test.js b/sites/directv.com.ar/directv.com.ar.test.js index 95aa923bf..d5c1a7dbd 100644 --- a/sites/directv.com.ar/directv.com.ar.test.js +++ b/sites/directv.com.ar/directv.com.ar.test.js @@ -1,79 +1,79 @@ -// npm run grab -- --site=directv.com.ar - -const { parser, url, request } = require('./directv.com.ar.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-06-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '207#A&EHD', - xmltv_id: 'AEHDSouth.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - filterParameters: { - day: 19, - time: 0, - minute: 0, - month: 6, - year: 2022, - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum: '207', - channelName: 'A&EHD' - } - }) -}) - -it('can parse response', () => { - const content = - '{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-06-19T03:00:00.000Z', - stop: '2022-06-19T03:15:00.000Z', - title: 'Chicas guapas', - description: - 'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.', - rating: { - system: 'MPA', - value: 'NR' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=directv.com.ar + +const { parser, url, request } = require('./directv.com.ar.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-06-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '207#A&EHD', + xmltv_id: 'AEHDSouth.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + filterParameters: { + day: 19, + time: 0, + minute: 0, + month: 6, + year: 2022, + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum: '207', + channelName: 'A&EHD' + } + }) +}) + +it('can parse response', () => { + const content = + '{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-06-19T03:00:00.000Z', + stop: '2022-06-19T03:15:00.000Z', + title: 'Chicas guapas', + description: + 'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.', + rating: { + system: 'MPA', + value: 'NR' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com.uy/directv.com.uy.config.js b/sites/directv.com.uy/directv.com.uy.config.js index f1a828ac4..c071406c9 100644 --- a/sites/directv.com.uy/directv.com.uy.config.js +++ b/sites/directv.com.uy/directv.com.uy.config.js @@ -1,85 +1,85 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'directv.com.uy', - days: 2, - url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' - }, - data({ channel, date }) { - const [channelNum, channelName] = channel.site_id.split('#') - - return { - filterParameters: { - day: date.date(), - time: 0, - minute: 0, - month: date.month() + 1, - year: date.year(), - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum, - channelName: channelName.replace('&', '&') - } - } - } - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - rating: parseRating(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} - -function parseStart(item) { - return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') -} - -function parseStop(item) { - return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') -} - -function parseItems(content, channel) { - if (!content) return [] - let [ChannelNumber, ChannelName] = channel.site_id.split('#') - ChannelName = ChannelName.replace('&', '&') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.d)) return [] - const channelData = data.d.find( - c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName - ) - - return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'directv.com.uy', + days: 2, + url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' + }, + data({ channel, date }) { + const [channelNum, channelName] = channel.site_id.split('#') + + return { + filterParameters: { + day: date.date(), + time: 0, + minute: 0, + month: date.month() + 1, + year: date.year(), + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum, + channelName: channelName.replace('&', '&') + } + } + } + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + rating: parseRating(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} + +function parseStart(item) { + return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') +} + +function parseStop(item) { + return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') +} + +function parseItems(content, channel) { + if (!content) return [] + let [ChannelNumber, ChannelName] = channel.site_id.split('#') + ChannelName = ChannelName.replace('&', '&') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.d)) return [] + const channelData = data.d.find( + c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName + ) + + return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] +} diff --git a/sites/directv.com.uy/directv.com.uy.test.js b/sites/directv.com.uy/directv.com.uy.test.js index 146748e2a..fa316c957 100644 --- a/sites/directv.com.uy/directv.com.uy.test.js +++ b/sites/directv.com.uy/directv.com.uy.test.js @@ -1,78 +1,78 @@ -// npm run grab -- --site=directv.com.uy - -const { parser, url, request } = require('./directv.com.uy.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '184#VTV', - xmltv_id: 'VTV.uy' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - filterParameters: { - day: 29, - time: 0, - minute: 0, - month: 8, - year: 2022, - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum: '184', - channelName: 'VTV' - } - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-29T03:00:00.000Z', - stop: '2022-08-29T05:00:00.000Z', - title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio', - description: - 'Jornada 5 del Torneo Clausura 2022. Peñarol recibe a Danubio en el estadio Campeón del Siglo. Los carboneros llevan 3 partidos sin caer (2PG 1PE), mientras que los franjeados acumulan 6 juegos sin derrotas (4PG 2PE).', - rating: { - system: 'MPA', - value: 'NR' - } - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=directv.com.uy + +const { parser, url, request } = require('./directv.com.uy.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '184#VTV', + xmltv_id: 'VTV.uy' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + filterParameters: { + day: 29, + time: 0, + minute: 0, + month: 8, + year: 2022, + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum: '184', + channelName: 'VTV' + } + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-29T03:00:00.000Z', + stop: '2022-08-29T05:00:00.000Z', + title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio', + description: + 'Jornada 5 del Torneo Clausura 2022. Peñarol recibe a Danubio en el estadio Campeón del Siglo. Los carboneros llevan 3 partidos sin caer (2PG 1PE), mientras que los franjeados acumulan 6 juegos sin derrotas (4PG 2PE).', + rating: { + system: 'MPA', + value: 'NR' + } + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com/directv.com.config.js b/sites/directv.com/directv.com.config.js index 3346a06c4..c048f27c3 100644 --- a/sites/directv.com/directv.com.config.js +++ b/sites/directv.com/directv.com.config.js @@ -1,113 +1,113 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'directv.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - headers: { - 'Accept-Language': 'en-US,en;q=0.5', - Connection: 'keep-alive' - } - }, - url({ date, channel }) { - const [channelId, childId] = channel.site_id.split('#') - return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}` - }, - async parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - if (item.programID === '-1') continue - const detail = await loadProgramDetail(item.programID) - const start = parseStart(item) - const stop = start.add(item.duration, 'm') - programs.push({ - title: item.title, - sub_title: item.episodeTitle, - description: parseDescription(detail), - rating: parseRating(item), - date: parseYear(detail), - category: item.subcategoryList, - season: item.seasonNumber, - episode: item.episodeNumber, - icon: parseIcon(item), - start, - stop - }) - } - - return programs - }, - async channels({ zip }) { - const html = await axios - .get('https://www.directv.com/guide', { - headers: { - cookie: `dtve-prospect-zip=${zip}` - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const script = $('#dtvClientData').html() - const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null] - const data = JSON.parse(json) - - let items = data.guideData.channels - - return items.map(item => { - return { - lang: 'en', - site_id: item.chNum, - name: item.chName - } - }) - } -} - -function parseDescription(detail) { - return detail ? detail.description : null -} -function parseYear(detail) { - return detail ? detail.releaseYear : null -} -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} -function parseIcon(item) { - return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null -} -function loadProgramDetail(programID) { - return axios - .get(`https://www.directv.com/json/program/flip/${programID}`) - .then(r => r.data) - .then(d => d.programDetail) - .catch(console.err) -} - -function parseStart(item) { - return dayjs.utc(item.airTime) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - if (!Array.isArray(data.schedule)) return [] - - const [, childId] = channel.site_id.split('#') - const channelData = data.schedule.find(i => i.chId == childId) - return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : [] -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'directv.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + headers: { + 'Accept-Language': 'en-US,en;q=0.5', + Connection: 'keep-alive' + } + }, + url({ date, channel }) { + const [channelId, childId] = channel.site_id.split('#') + return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}` + }, + async parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + if (item.programID === '-1') continue + const detail = await loadProgramDetail(item.programID) + const start = parseStart(item) + const stop = start.add(item.duration, 'm') + programs.push({ + title: item.title, + sub_title: item.episodeTitle, + description: parseDescription(detail), + rating: parseRating(item), + date: parseYear(detail), + category: item.subcategoryList, + season: item.seasonNumber, + episode: item.episodeNumber, + icon: parseIcon(item), + start, + stop + }) + } + + return programs + }, + async channels({ zip }) { + const html = await axios + .get('https://www.directv.com/guide', { + headers: { + cookie: `dtve-prospect-zip=${zip}` + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const script = $('#dtvClientData').html() + const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null] + const data = JSON.parse(json) + + let items = data.guideData.channels + + return items.map(item => { + return { + lang: 'en', + site_id: item.chNum, + name: item.chName + } + }) + } +} + +function parseDescription(detail) { + return detail ? detail.description : null +} +function parseYear(detail) { + return detail ? detail.releaseYear : null +} +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} +function parseIcon(item) { + return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null +} +function loadProgramDetail(programID) { + return axios + .get(`https://www.directv.com/json/program/flip/${programID}`) + .then(r => r.data) + .then(d => d.programDetail) + .catch(console.err) +} + +function parseStart(item) { + return dayjs.utc(item.airTime) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + if (!Array.isArray(data.schedule)) return [] + + const [, childId] = channel.site_id.split('#') + const channelData = data.schedule.find(i => i.chId == childId) + return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : [] +} diff --git a/sites/directv.com/directv.com.test.js b/sites/directv.com/directv.com.test.js index d84d80293..0b201fc05 100644 --- a/sites/directv.com/directv.com.test.js +++ b/sites/directv.com/directv.com.test.js @@ -1,98 +1,98 @@ -// node ./scripts/commands/parse-channels.js --config=./sites/directv.com/directv.com.config.js --output=./sites/directv.com/directv.com.channels.xml --set=zip:10001 -// npm run grab -- --site=directv.com - -const { parser, url } = require('./directv.com.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '249#249', - xmltv_id: 'ComedyCentralEast.us' -} - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249' - ) -}) - -it('can parse response', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if (url === 'https://www.directv.com/json/program/flip/MV001173520000') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, channel }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-01-14T23:00:00.000Z', - stop: '2023-01-15T01:00:00.000Z', - title: 'Men in Black II', - description: - 'Kay (Tommy Lee Jones) and Jay (Will Smith) reunite to provide our best line of defense against a seductress who levels the toughest challenge yet to the MIBs mission statement: protecting the earth from the scum of the universe. While investigating a routine crime, Jay uncovers a plot masterminded by Serleena (Boyle), a Kylothian monster who disguises herself as a lingerie model. When Serleena takes the MIB building hostage, there is only one person Jay can turn to -- his former MIB partner.', - date: '2002', - icon: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg', - category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'], - rating: { - system: 'MPA', - value: 'TV14' - } - }, - { - start: '2023-01-15T06:00:00.000Z', - stop: '2023-01-15T06:30:00.000Z', - title: 'South Park', - sub_title: 'Goth Kids 3: Dawn of the Posers', - description: 'The goth kids are sent to a camp for troubled children.', - icon: 'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg', - category: ['Series', 'Animation', 'Comedy'], - season: 17, - episode: 4, - rating: { - system: 'MPA', - value: 'TVMA' - } - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) - parser({ content, channel }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// node ./scripts/commands/parse-channels.js --config=./sites/directv.com/directv.com.config.js --output=./sites/directv.com/directv.com.channels.xml --set=zip:10001 +// npm run grab -- --site=directv.com + +const { parser, url } = require('./directv.com.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '249#249', + xmltv_id: 'ComedyCentralEast.us' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249' + ) +}) + +it('can parse response', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if (url === 'https://www.directv.com/json/program/flip/MV001173520000') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, channel }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-01-14T23:00:00.000Z', + stop: '2023-01-15T01:00:00.000Z', + title: 'Men in Black II', + description: + 'Kay (Tommy Lee Jones) and Jay (Will Smith) reunite to provide our best line of defense against a seductress who levels the toughest challenge yet to the MIBs mission statement: protecting the earth from the scum of the universe. While investigating a routine crime, Jay uncovers a plot masterminded by Serleena (Boyle), a Kylothian monster who disguises herself as a lingerie model. When Serleena takes the MIB building hostage, there is only one person Jay can turn to -- his former MIB partner.', + date: '2002', + icon: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg', + category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'], + rating: { + system: 'MPA', + value: 'TV14' + } + }, + { + start: '2023-01-15T06:00:00.000Z', + stop: '2023-01-15T06:30:00.000Z', + title: 'South Park', + sub_title: 'Goth Kids 3: Dawn of the Posers', + description: 'The goth kids are sent to a camp for troubled children.', + icon: 'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg', + category: ['Series', 'Animation', 'Comedy'], + season: 17, + episode: 4, + rating: { + system: 'MPA', + value: 'TVMA' + } + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) + parser({ content, channel }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/dishtv.in/dishtv.in.config.js b/sites/dishtv.in/dishtv.in.config.js index 1df159983..cab656466 100644 --- a/sites/dishtv.in/dishtv.in.config.js +++ b/sites/dishtv.in/dishtv.in.config.js @@ -1,145 +1,145 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'dishtv.in', - days: 2, - url: 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', - request: { - method: 'POST', - data({ channel, date }) { - return { - Channelarr: channel.site_id, - fromdate: date.format('YYYYMMDDHHmm'), - todate: date.add(1, 'd').format('YYYYMMDDHHmm') - } - } - }, - parser: function ({ content, date }) { - let programs = [] - const data = parseContent(content) - const items = parseItems(data) - items.forEach(item => { - const title = parseTitle(item) - const start = parseStart(item, date) - const stop = parseStop(item, start) - if (title === 'No Information Available') return - - programs.push({ - title, - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - }, - async channels() { - const channelguide = await axios - .get('https://www.dishtv.in/channelguide/') - .then(r => r.data) - .catch(console.log) - const $channelguide = cheerio.load(channelguide) - - let ids = [] - $channelguide('#MainContent_recordPagging li').each((i, item) => { - const onclick = $channelguide(item).find('a').attr('onclick') - const [, list] = onclick.match(/ShowNextPageResult\('([^']+)/) || [null, null] - - ids = ids.concat(list.split(',')) - }) - ids = ids.filter(Boolean) - - const channels = {} - const channelList = await axios - .post('https://www.dishtv.in/WebServiceMethod.aspx/GetChannelListFromMobileAPI', { - strChannel: '' - }) - .then(r => r.data) - .catch(console.log) - const $channelList = cheerio.load(channelList.d) - $channelList('#tblpackChnl > div').each((i, item) => { - let num = $channelList(item).find('p:nth-child(2)').text().trim() - const name = $channelList(item).find('p').first().text().trim() - - if (num === '') return - - channels[parseInt(num)] = { - name - } - }) - - const date = dayjs().add(1, 'd') - const promises = [] - for (let id of ids) { - const promise = axios - .post( - 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', - { - Channelarr: id, - fromdate: date.format('YYYYMMDD[0000]'), - todate: date.format('YYYYMMDD[2300]') - }, - { timeout: 5000 } - ) - .then(r => r.data) - .then(data => { - const $channelGuide = cheerio.load(data.d) - - const num = $channelGuide('.cnl-fav > a > span').text().trim() - - if (channels[num]) { - channels[num].site_id = id - } - }) - .catch(console.log) - - promises.push(promise) - } - - await Promise.allSettled(promises) - - return Object.values(channels) - } -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('a').text() -} - -function parseStart(item) { - const $ = cheerio.load(item) - const onclick = $('i.fa-circle').attr('onclick') - const [, time] = onclick.match(/RecordingEnteryOpen\('.*','.*','(.*)','.*',.*\)/) - - return dayjs.tz(time, 'YYYYMMDDHHmm', 'Asia/Kolkata') -} - -function parseStop(item, start) { - const $ = cheerio.load(item) - const duration = $('*').data('time') - - return start.add(duration, 'm') -} - -function parseContent(content) { - const data = JSON.parse(content) - - return data.d -} - -function parseItems(data) { - const $ = cheerio.load(data) - - return $('.datatime').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'dishtv.in', + days: 2, + url: 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', + request: { + method: 'POST', + data({ channel, date }) { + return { + Channelarr: channel.site_id, + fromdate: date.format('YYYYMMDDHHmm'), + todate: date.add(1, 'd').format('YYYYMMDDHHmm') + } + } + }, + parser: function ({ content, date }) { + let programs = [] + const data = parseContent(content) + const items = parseItems(data) + items.forEach(item => { + const title = parseTitle(item) + const start = parseStart(item, date) + const stop = parseStop(item, start) + if (title === 'No Information Available') return + + programs.push({ + title, + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + }, + async channels() { + const channelguide = await axios + .get('https://www.dishtv.in/channelguide/') + .then(r => r.data) + .catch(console.log) + const $channelguide = cheerio.load(channelguide) + + let ids = [] + $channelguide('#MainContent_recordPagging li').each((i, item) => { + const onclick = $channelguide(item).find('a').attr('onclick') + const [, list] = onclick.match(/ShowNextPageResult\('([^']+)/) || [null, null] + + ids = ids.concat(list.split(',')) + }) + ids = ids.filter(Boolean) + + const channels = {} + const channelList = await axios + .post('https://www.dishtv.in/WebServiceMethod.aspx/GetChannelListFromMobileAPI', { + strChannel: '' + }) + .then(r => r.data) + .catch(console.log) + const $channelList = cheerio.load(channelList.d) + $channelList('#tblpackChnl > div').each((i, item) => { + let num = $channelList(item).find('p:nth-child(2)').text().trim() + const name = $channelList(item).find('p').first().text().trim() + + if (num === '') return + + channels[parseInt(num)] = { + name + } + }) + + const date = dayjs().add(1, 'd') + const promises = [] + for (let id of ids) { + const promise = axios + .post( + 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', + { + Channelarr: id, + fromdate: date.format('YYYYMMDD[0000]'), + todate: date.format('YYYYMMDD[2300]') + }, + { timeout: 5000 } + ) + .then(r => r.data) + .then(data => { + const $channelGuide = cheerio.load(data.d) + + const num = $channelGuide('.cnl-fav > a > span').text().trim() + + if (channels[num]) { + channels[num].site_id = id + } + }) + .catch(console.log) + + promises.push(promise) + } + + await Promise.allSettled(promises) + + return Object.values(channels) + } +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('a').text() +} + +function parseStart(item) { + const $ = cheerio.load(item) + const onclick = $('i.fa-circle').attr('onclick') + const [, time] = onclick.match(/RecordingEnteryOpen\('.*','.*','(.*)','.*',.*\)/) + + return dayjs.tz(time, 'YYYYMMDDHHmm', 'Asia/Kolkata') +} + +function parseStop(item, start) { + const $ = cheerio.load(item) + const duration = $('*').data('time') + + return start.add(duration, 'm') +} + +function parseContent(content) { + const data = JSON.parse(content) + + return data.d +} + +function parseItems(data) { + const $ = cheerio.load(data) + + return $('.datatime').toArray() +} diff --git a/sites/dishtv.in/dishtv.in.test.js b/sites/dishtv.in/dishtv.in.test.js index 124ffd561..74ed38613 100644 --- a/sites/dishtv.in/dishtv.in.test.js +++ b/sites/dishtv.in/dishtv.in.test.js @@ -1,45 +1,45 @@ -// npm run channels:parse -- --config=./sites/dishtv.in/dishtv.in.config.js --output=./sites/dishtv.in/dishtv.in.channels.xml -// npm run grab -- --site=dishtv.in - -const { parser, url, request } = require('./dishtv.in.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '10000000075992337', xmltv_id: 'WomensActive.in' } -const content = - '{"d":"\\u003cdiv class=\\"pgrid\\"\\u003e\\u003cdiv class=\\"img sm-30 grid\\"\\u003e\\u003cimg class=\\"chnl-logo\\" src=\\"http://imagesdishtvd2h.whatsonindia.com/dasimages/channel/landscape/360x270/hiyj8ndf.png\\" onclick=\\"ShowChannelGuid(\\u0027womens-active\\u0027,\\u002710000000075992337\\u0027);\\" /\\u003e\\u003cdiv class=\\"cnl-fav\\"\\u003e\\u003ca href=\\"javascript:;\\"\\u003e\\u003cem\\u003ech. no\\u003c/em\\u003e\\u003cspan\\u003e117\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/div\\u003e\\u003ci class=\\"fa fa-heart Set_Favourite_Channel\\" aria-hidden=\\"true\\" title=\\"Set womens active channel as your favourite channel\\" onclick=\\"SetFavouriteChannel();\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003cdiv class=\\"grid-wrap\\"\\u003e\\u003cdiv class=\\"sm-30 grid datatime\\" data-time=\\"24\\" data-starttime=\\"12:00 AM\\" data-endttime=\\"12:24 AM\\" data-reamintime=\\"0\\"\\u003e\\u003ca title=\\"Event Name: Cynthia Williams - Diwali Look Part 01\\r\\nStart Time: 12:00 AM\\r\\nDuration: 24min\\r\\nSynopsis: Learn diwali look by cynthia williams p1\\r\\n\\" href=\\"javascript:;\\" onclick=\\"ShowCurrentTime(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111051200\\u0027)\\"\\u003eCynthia Williams - Diwali Look Part 01\\u003c/a\\u003e\\u003cdiv class=\\"cnlSerialIcon\\"\\u003e\\u003ci class=\\"fa fa-heart\\" aria-hidden=\\"true\\" title=\\"Set Favourite Serial\\" onclick=\\"SetFavouriteShow();\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-clock-o\\" aria-hidden=\\"true\\" title=\\"Reminder Serial\\" onclick=\\"ReminderEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027)\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-circle\\" aria-hidden=\\"true\\" title=\\"Record Serial\\" onclick=\\"RecordingEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027,30000000550913679)\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e"}' - -it('can generate valid url', () => { - expect(url).toBe( - 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram' - ) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result).toMatchObject({ - Channelarr: '10000000075992337', - fromdate: '202111050000', - todate: '202111060000' - }) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: 'Thu, 04 Nov 2021 18:30:00 GMT', - stop: 'Thu, 04 Nov 2021 18:54:00 GMT', - title: 'Cynthia Williams - Diwali Look Part 01' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{"d":""}' }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/dishtv.in/dishtv.in.config.js --output=./sites/dishtv.in/dishtv.in.channels.xml +// npm run grab -- --site=dishtv.in + +const { parser, url, request } = require('./dishtv.in.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '10000000075992337', xmltv_id: 'WomensActive.in' } +const content = + '{"d":"\\u003cdiv class=\\"pgrid\\"\\u003e\\u003cdiv class=\\"img sm-30 grid\\"\\u003e\\u003cimg class=\\"chnl-logo\\" src=\\"http://imagesdishtvd2h.whatsonindia.com/dasimages/channel/landscape/360x270/hiyj8ndf.png\\" onclick=\\"ShowChannelGuid(\\u0027womens-active\\u0027,\\u002710000000075992337\\u0027);\\" /\\u003e\\u003cdiv class=\\"cnl-fav\\"\\u003e\\u003ca href=\\"javascript:;\\"\\u003e\\u003cem\\u003ech. no\\u003c/em\\u003e\\u003cspan\\u003e117\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/div\\u003e\\u003ci class=\\"fa fa-heart Set_Favourite_Channel\\" aria-hidden=\\"true\\" title=\\"Set womens active channel as your favourite channel\\" onclick=\\"SetFavouriteChannel();\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003cdiv class=\\"grid-wrap\\"\\u003e\\u003cdiv class=\\"sm-30 grid datatime\\" data-time=\\"24\\" data-starttime=\\"12:00 AM\\" data-endttime=\\"12:24 AM\\" data-reamintime=\\"0\\"\\u003e\\u003ca title=\\"Event Name: Cynthia Williams - Diwali Look Part 01\\r\\nStart Time: 12:00 AM\\r\\nDuration: 24min\\r\\nSynopsis: Learn diwali look by cynthia williams p1\\r\\n\\" href=\\"javascript:;\\" onclick=\\"ShowCurrentTime(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111051200\\u0027)\\"\\u003eCynthia Williams - Diwali Look Part 01\\u003c/a\\u003e\\u003cdiv class=\\"cnlSerialIcon\\"\\u003e\\u003ci class=\\"fa fa-heart\\" aria-hidden=\\"true\\" title=\\"Set Favourite Serial\\" onclick=\\"SetFavouriteShow();\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-clock-o\\" aria-hidden=\\"true\\" title=\\"Reminder Serial\\" onclick=\\"ReminderEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027)\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-circle\\" aria-hidden=\\"true\\" title=\\"Record Serial\\" onclick=\\"RecordingEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027,30000000550913679)\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e"}' + +it('can generate valid url', () => { + expect(url).toBe( + 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram' + ) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result).toMatchObject({ + Channelarr: '10000000075992337', + fromdate: '202111050000', + todate: '202111060000' + }) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: 'Thu, 04 Nov 2021 18:30:00 GMT', + stop: 'Thu, 04 Nov 2021 18:54:00 GMT', + title: 'Cynthia Williams - Diwali Look Part 01' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{"d":""}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/dsmart.com.tr/dsmart.com.tr.config.js b/sites/dsmart.com.tr/dsmart.com.tr.config.js index d545e2267..20de68e13 100644 --- a/sites/dsmart.com.tr/dsmart.com.tr.config.js +++ b/sites/dsmart.com.tr/dsmart.com.tr.config.js @@ -1,104 +1,104 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.dsmart.com.tr/api/v1/public/epg/schedules' - -module.exports = { - site: 'dsmart.com.tr', - days: 2, - url({ date, channel }) { - const [page] = channel.site_id.split('#') - - return `${API_ENDPOINT}?page=${page}&limit=1&day=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start - if (prev) { - start = parseStart(item, prev.stop) - } else { - start = parseStart(item, dayjs.utc(item.day)) - } - let duration = parseDuration(item) - let stop = start.add(duration, 's') - - programs.push({ - title: item.program_name, - category: parseCategory(item), - description: item.description.trim(), - start, - stop - }) - }) - - return programs - }, - async channels() { - const perPage = 1 - const totalChannels = 210 - const pages = Math.ceil(totalChannels / perPage) - - const channels = [] - for (let i in Array(pages).fill(0)) { - const page = parseInt(i) + 1 - const url = `${API_ENDPOINT}?page=${page}&limit=${perPage}&day=${dayjs().format( - 'YYYY-MM-DD' - )}` - let offset = i * perPage - await axios - .get(url) - .then(r => r.data) - .then(data => { - offset++ - if (data && data.data && Array.isArray(data.data.channels)) { - data.data.channels.forEach((item, j) => { - const index = offset + j - channels.push({ - lang: 'tr', - name: item.channel_name, - site_id: index + '#' + item._id - }) - }) - } - }) - .catch(err => { - console.log(err.message) - }) - } - - return channels - } -} - -function parseCategory(item) { - return item.genre !== '0' ? item.genre : null -} - -function parseStart(item, date) { - const time = dayjs.utc(item.start_date) - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time.format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss') -} - -function parseDuration(item) { - const [, H, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)$/) - - return parseInt(H) * 3600 + parseInt(mm) * 60 + parseInt(ss) -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !data.data || !Array.isArray(data.data.channels)) return null - const channelData = data.data.channels.find(i => i._id == channelId) - - return channelData && Array.isArray(channelData.schedule) ? channelData.schedule : [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.dsmart.com.tr/api/v1/public/epg/schedules' + +module.exports = { + site: 'dsmart.com.tr', + days: 2, + url({ date, channel }) { + const [page] = channel.site_id.split('#') + + return `${API_ENDPOINT}?page=${page}&limit=1&day=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start + if (prev) { + start = parseStart(item, prev.stop) + } else { + start = parseStart(item, dayjs.utc(item.day)) + } + let duration = parseDuration(item) + let stop = start.add(duration, 's') + + programs.push({ + title: item.program_name, + category: parseCategory(item), + description: item.description.trim(), + start, + stop + }) + }) + + return programs + }, + async channels() { + const perPage = 1 + const totalChannels = 210 + const pages = Math.ceil(totalChannels / perPage) + + const channels = [] + for (let i in Array(pages).fill(0)) { + const page = parseInt(i) + 1 + const url = `${API_ENDPOINT}?page=${page}&limit=${perPage}&day=${dayjs().format( + 'YYYY-MM-DD' + )}` + let offset = i * perPage + await axios + .get(url) + .then(r => r.data) + .then(data => { + offset++ + if (data && data.data && Array.isArray(data.data.channels)) { + data.data.channels.forEach((item, j) => { + const index = offset + j + channels.push({ + lang: 'tr', + name: item.channel_name, + site_id: index + '#' + item._id + }) + }) + } + }) + .catch(err => { + console.log(err.message) + }) + } + + return channels + } +} + +function parseCategory(item) { + return item.genre !== '0' ? item.genre : null +} + +function parseStart(item, date) { + const time = dayjs.utc(item.start_date) + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time.format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss') +} + +function parseDuration(item) { + const [, H, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)$/) + + return parseInt(H) * 3600 + parseInt(mm) * 60 + parseInt(ss) +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !data.data || !Array.isArray(data.data.channels)) return null + const channelData = data.data.channels.find(i => i._id == channelId) + + return channelData && Array.isArray(channelData.schedule) ? channelData.schedule : [] +} diff --git a/sites/dsmart.com.tr/dsmart.com.tr.test.js b/sites/dsmart.com.tr/dsmart.com.tr.test.js index 0b2cf6690..0bfdb603b 100644 --- a/sites/dsmart.com.tr/dsmart.com.tr.test.js +++ b/sites/dsmart.com.tr/dsmart.com.tr.test.js @@ -1,68 +1,68 @@ -// npm run channels:parse -- --config=./sites/dsmart.com.tr/dsmart.com.tr.config.js --output=./sites/dsmart.com.tr/dsmart.com.tr.channels.xml -// npm run grab -- --site=dsmart.com.tr - -const { parser, url } = require('./dsmart.com.tr.config.js') -const dayjs = require('dayjs') -const fs = require('fs') -const path = require('path') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3#5fe07d7acfef0b1593275751', - xmltv_id: 'SinemaTV.tr' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=3&limit=1&day=2023-01-16' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-15T22:00:00.000Z', - stop: '2023-01-15T23:45:00.000Z', - title: 'Bizi Ayıran Her Şey', - category: 'sinema/genel', - description: - 'Issızlığın ortasında yer alan orta sınıf bir evde bir anne kız yaşamaktadır. Çevrelerindeki taşları insanlarla yaşadıkları çatışmalar, anne-kızın hayatını olumsuz yönde etkilemektedir. Kızının ansızın ortadan kaybolması, bu çatışmaların seviyesini artıracak ve anne, kızını bulmak için her türlü yola başvuracaktır.' - }) - - expect(results[1]).toMatchObject({ - start: '2023-01-15T23:45:00.000Z', - stop: '2023-01-16T01:30:00.000Z', - title: 'Pixie', - category: 'sinema/genel', - description: - 'Annesinin intikamını almak isteyen Pixie, dahiyane bir soygun planlar. Fakat işler planladığı gibi gitmeyince kendini İrlanda’nın vahşi gangsterleri tarafından kovalanan iki adamla birlikte kaçarken bulur.' - }) - - expect(results[12]).toMatchObject({ - start: '2023-01-16T20:30:00.000Z', - stop: '2023-01-16T22:30:00.000Z', - title: 'Seberg', - category: 'sinema/genel', - description: - 'Başrolünde ünlü yıldız Kristen Stewart’ın yer aldığı politik gerilim, 1960’ların sonunda insan hakları aktivisti Hakim Jamal ile yaşadığı politik ve romantik ilişki sebebiyle FBI tarafından hedef alınan, Fransız Yeni Dalgası’nın sevilen yüzü ve Serseri Aşıklar’ın yıldızı Jean Seberg’ün çarpıcı hikayesini anlatıyor.' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/dsmart.com.tr/dsmart.com.tr.config.js --output=./sites/dsmart.com.tr/dsmart.com.tr.channels.xml +// npm run grab -- --site=dsmart.com.tr + +const { parser, url } = require('./dsmart.com.tr.config.js') +const dayjs = require('dayjs') +const fs = require('fs') +const path = require('path') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3#5fe07d7acfef0b1593275751', + xmltv_id: 'SinemaTV.tr' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=3&limit=1&day=2023-01-16' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-15T22:00:00.000Z', + stop: '2023-01-15T23:45:00.000Z', + title: 'Bizi Ayıran Her Şey', + category: 'sinema/genel', + description: + 'Issızlığın ortasında yer alan orta sınıf bir evde bir anne kız yaşamaktadır. Çevrelerindeki taşları insanlarla yaşadıkları çatışmalar, anne-kızın hayatını olumsuz yönde etkilemektedir. Kızının ansızın ortadan kaybolması, bu çatışmaların seviyesini artıracak ve anne, kızını bulmak için her türlü yola başvuracaktır.' + }) + + expect(results[1]).toMatchObject({ + start: '2023-01-15T23:45:00.000Z', + stop: '2023-01-16T01:30:00.000Z', + title: 'Pixie', + category: 'sinema/genel', + description: + 'Annesinin intikamını almak isteyen Pixie, dahiyane bir soygun planlar. Fakat işler planladığı gibi gitmeyince kendini İrlanda’nın vahşi gangsterleri tarafından kovalanan iki adamla birlikte kaçarken bulur.' + }) + + expect(results[12]).toMatchObject({ + start: '2023-01-16T20:30:00.000Z', + stop: '2023-01-16T22:30:00.000Z', + title: 'Seberg', + category: 'sinema/genel', + description: + 'Başrolünde ünlü yıldız Kristen Stewart’ın yer aldığı politik gerilim, 1960’ların sonunda insan hakları aktivisti Hakim Jamal ile yaşadığı politik ve romantik ilişki sebebiyle FBI tarafından hedef alınan, Fransız Yeni Dalgası’nın sevilen yüzü ve Serseri Aşıklar’ın yıldızı Jean Seberg’ün çarpıcı hikayesini anlatıyor.' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/dstv.com/dstv.com.config.js b/sites/dstv.com/dstv.com.config.js index 8f7e62aea..02472ae34 100644 --- a/sites/dstv.com/dstv.com.config.js +++ b/sites/dstv.com/dstv.com.config.js @@ -1,101 +1,101 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' - -module.exports = { - site: 'dstv.com', - days: 2, - request: { - cache: { - ttl: 3 * 60 * 60 * 1000, // 3h - interpretHeader: false - } - }, - url: function ({ channel, date }) { - const [region] = channel.site_id.split('#') - const packageName = region === 'nga' ? '&package=DStv%20Premium' : '' - - return `${API_ENDPOINT}/GetProgrammes?d=${date.format( - 'YYYY-MM-DD' - )}${packageName}&country=${region}` - }, - async parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - for (const item of items) { - const details = await loadProgramDetails(item) - programs.push({ - title: item.Title, - description: parseDescription(details), - icon: parseIcon(details), - category: parseCategory(details), - start: parseTime(item.StartTime, channel), - stop: parseTime(item.EndTime, channel) - }) - } - - return programs - }, - async channels({ country }) { - const data = await axios - .get(`${API_ENDPOINT}/GetProgrammes?d=2022-03-10&package=DStv%20Premium&country=${country}`) - .then(r => r.data) - .catch(console.log) - - return data.Channels.map(item => { - return { - site_id: `${country}#${item.Number}`, - name: item.Name - } - }) - } -} - -function parseTime(time, channel) { - const [region] = channel.site_id.split('#') - const tz = { - zaf: 'Africa/Johannesburg', - nga: 'Africa/Lagos' - } - - return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region]) -} - -function parseDescription(details) { - return details ? details.Synopsis : null -} - -function parseIcon(details) { - return details ? details.ThumbnailUri : null -} - -function parseCategory(details) { - return details ? details.SubGenres : null -} - -async function loadProgramDetails(item) { - const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}` - - return axios - .get(url) - .then(r => r.data) - .catch(console.error) -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.Channels)) return [] - const channelData = data.Channels.find(c => c.Number === channelId) - if (!channelData || !Array.isArray(channelData.Programmes)) return [] - - return channelData.Programmes -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' + +module.exports = { + site: 'dstv.com', + days: 2, + request: { + cache: { + ttl: 3 * 60 * 60 * 1000, // 3h + interpretHeader: false + } + }, + url: function ({ channel, date }) { + const [region] = channel.site_id.split('#') + const packageName = region === 'nga' ? '&package=DStv%20Premium' : '' + + return `${API_ENDPOINT}/GetProgrammes?d=${date.format( + 'YYYY-MM-DD' + )}${packageName}&country=${region}` + }, + async parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + for (const item of items) { + const details = await loadProgramDetails(item) + programs.push({ + title: item.Title, + description: parseDescription(details), + icon: parseIcon(details), + category: parseCategory(details), + start: parseTime(item.StartTime, channel), + stop: parseTime(item.EndTime, channel) + }) + } + + return programs + }, + async channels({ country }) { + const data = await axios + .get(`${API_ENDPOINT}/GetProgrammes?d=2022-03-10&package=DStv%20Premium&country=${country}`) + .then(r => r.data) + .catch(console.log) + + return data.Channels.map(item => { + return { + site_id: `${country}#${item.Number}`, + name: item.Name + } + }) + } +} + +function parseTime(time, channel) { + const [region] = channel.site_id.split('#') + const tz = { + zaf: 'Africa/Johannesburg', + nga: 'Africa/Lagos' + } + + return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region]) +} + +function parseDescription(details) { + return details ? details.Synopsis : null +} + +function parseIcon(details) { + return details ? details.ThumbnailUri : null +} + +function parseCategory(details) { + return details ? details.SubGenres : null +} + +async function loadProgramDetails(item) { + const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}` + + return axios + .get(url) + .then(r => r.data) + .catch(console.error) +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.Channels)) return [] + const channelData = data.Channels.find(c => c.Number === channelId) + if (!channelData || !Array.isArray(channelData.Programmes)) return [] + + return channelData.Programmes +} diff --git a/sites/dstv.com/dstv.com.test.js b/sites/dstv.com/dstv.com.test.js index b18bb6abf..caf68f9a2 100644 --- a/sites/dstv.com/dstv.com.test.js +++ b/sites/dstv.com/dstv.com.test.js @@ -1,112 +1,112 @@ -// npm run channels:parse -- --config=./sites/dstv.com/dstv.com.config.js --output=./sites/dstv.com/dstv.com.channels.xml --set=country:zaf -// npm run grab -- --site=dstv.com - -const { parser, url } = require('./dstv.com.config.js') -const axios = require('axios') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' - -const date = dayjs.utc('2022-11-22', 'YYYY-MM-DD').startOf('d') -const channelZA = { - site_id: 'zaf#201', - xmltv_id: 'SuperSportGrandstand.za' -} -const channelNG = { - site_id: 'nga#201', - xmltv_id: 'SuperSportGrandstand.za' -} - -it('can generate valid url for zaf', () => { - expect(url({ channel: channelZA, date })).toBe( - `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&country=zaf` - ) -}) - -it('can generate valid url for nga', () => { - expect(url({ channel: channelNG, date })).toBe( - `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&package=DStv%20Premium&country=nga` - ) -}) - -it('can parse response for ZA', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zaf.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_ENDPOINT}/GetProgramme?id=8b237235-aa17-4bb8-9ea6-097e7a813336`) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_zaf.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel: channelZA }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-21T23:00:00.000Z', - stop: '2022-11-22T00:00:00.000Z', - title: 'UFC FN HL: Nzechukwu v Cutelaba', - description: - "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", - icon: 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', - category: ['All Sport', 'Mixed Martial Arts'] - }) -}) - -it('can parse response for NG', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_nga.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_ENDPOINT}/GetProgramme?id=6d58931e-2192-486a-a202-14720136d204`) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_nga.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel: channelNG }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-21T23:00:00.000Z', - stop: '2022-11-22T00:00:00.000Z', - title: 'UFC FN HL: Nzechukwu v Cutelaba', - description: - "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", - icon: 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', - category: ['All Sport', 'Mixed Martial Arts'] - }) -}) - -it('can handle empty guide', done => { - parser({ - content: '{"Total":0,"Channels":[]}', - channel: channelZA - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run channels:parse -- --config=./sites/dstv.com/dstv.com.config.js --output=./sites/dstv.com/dstv.com.channels.xml --set=country:zaf +// npm run grab -- --site=dstv.com + +const { parser, url } = require('./dstv.com.config.js') +const axios = require('axios') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' + +const date = dayjs.utc('2022-11-22', 'YYYY-MM-DD').startOf('d') +const channelZA = { + site_id: 'zaf#201', + xmltv_id: 'SuperSportGrandstand.za' +} +const channelNG = { + site_id: 'nga#201', + xmltv_id: 'SuperSportGrandstand.za' +} + +it('can generate valid url for zaf', () => { + expect(url({ channel: channelZA, date })).toBe( + `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&country=zaf` + ) +}) + +it('can generate valid url for nga', () => { + expect(url({ channel: channelNG, date })).toBe( + `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&package=DStv%20Premium&country=nga` + ) +}) + +it('can parse response for ZA', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zaf.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_ENDPOINT}/GetProgramme?id=8b237235-aa17-4bb8-9ea6-097e7a813336`) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_zaf.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel: channelZA }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-21T23:00:00.000Z', + stop: '2022-11-22T00:00:00.000Z', + title: 'UFC FN HL: Nzechukwu v Cutelaba', + description: + "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", + icon: 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', + category: ['All Sport', 'Mixed Martial Arts'] + }) +}) + +it('can parse response for NG', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_nga.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_ENDPOINT}/GetProgramme?id=6d58931e-2192-486a-a202-14720136d204`) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_nga.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel: channelNG }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-21T23:00:00.000Z', + stop: '2022-11-22T00:00:00.000Z', + title: 'UFC FN HL: Nzechukwu v Cutelaba', + description: + "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", + icon: 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', + category: ['All Sport', 'Mixed Martial Arts'] + }) +}) + +it('can handle empty guide', done => { + parser({ + content: '{"Total":0,"Channels":[]}', + channel: channelZA + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/elcinema.com/elcinema.com.config.js b/sites/elcinema.com/elcinema.com.config.js index 878362f98..10b035855 100644 --- a/sites/elcinema.com/elcinema.com.config.js +++ b/sites/elcinema.com/elcinema.com.config.js @@ -1,118 +1,118 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -require('dayjs/locale/ar') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'elcinema.com', - days: 2, - url({ channel }) { - const lang = channel.lang === 'en' ? 'en/' : '/' - - return `https://elcinema.com/${lang}tvguide/${channel.site_id}/` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const start = parseStart(item, date) - const duration = parseDuration(item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - category: parseCategory(item), - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - } -} - -function parseIcon(item) { - const $ = cheerio.load(item) - const imgSrc = - $('.row > div.columns.small-3.large-1 > a > img').data('src') || - $('.row > div.columns.small-5.large-1 > img').data('src') - - return imgSrc || null -} - -function parseCategory(item) { - const $ = cheerio.load(item) - const category = $('.row > div.columns.small-6.large-3 > ul > li:nth-child(2)').text() - - return category.replace(/\(\d+\)/, '').trim() || null -} - -function parseDuration(item) { - const $ = cheerio.load(item) - const duration = - $('.row > div.columns.small-3.large-2 > ul > li:nth-child(2) > span').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2) > span').text() - - return duration.replace(/\D/g, '') || '' -} - -function parseStart(item, initDate) { - const $ = cheerio.load(item) - let time = - $('.row > div.columns.small-3.large-2 > ul > li:nth-child(1)').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2)').text() || - '' - - time = time - .replace(/\[.*\]/, '') - .replace('مساءً', 'PM') - .replace('صباحًا', 'AM') - .trim() - - time = `${initDate.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD hh:mm A', dayjs.tz.guess()) -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return ( - $('.row > div.columns.small-6.large-3 > ul > li:nth-child(1) > a').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(1)').text() || - null - ) -} - -function parseDescription(item) { - const $ = cheerio.load(item) - const excerpt = $('.row > div.columns.small-12.large-6 > ul > li:nth-child(3)').text() || '' - - return excerpt.replace('...اقرأ المزيد', '').replace('...Read more', '') -} - -function parseItems(content, channel, date) { - const $ = cheerio.load(content) - - const dateString = date.locale(channel.lang).format('dddd D') - - const list = $('.dates') - .filter((i, el) => { - let parsedDateString = $(el).text().trim() - parsedDateString = parsedDateString.replace(/\s\s+/g, ' ') - - return parsedDateString.includes(dateString) - }) - .first() - .parent() - .next() - - return $('.padded-half', list).toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +require('dayjs/locale/ar') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'elcinema.com', + days: 2, + url({ channel }) { + const lang = channel.lang === 'en' ? 'en/' : '/' + + return `https://elcinema.com/${lang}tvguide/${channel.site_id}/` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const start = parseStart(item, date) + const duration = parseDuration(item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + category: parseCategory(item), + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + } +} + +function parseIcon(item) { + const $ = cheerio.load(item) + const imgSrc = + $('.row > div.columns.small-3.large-1 > a > img').data('src') || + $('.row > div.columns.small-5.large-1 > img').data('src') + + return imgSrc || null +} + +function parseCategory(item) { + const $ = cheerio.load(item) + const category = $('.row > div.columns.small-6.large-3 > ul > li:nth-child(2)').text() + + return category.replace(/\(\d+\)/, '').trim() || null +} + +function parseDuration(item) { + const $ = cheerio.load(item) + const duration = + $('.row > div.columns.small-3.large-2 > ul > li:nth-child(2) > span').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2) > span').text() + + return duration.replace(/\D/g, '') || '' +} + +function parseStart(item, initDate) { + const $ = cheerio.load(item) + let time = + $('.row > div.columns.small-3.large-2 > ul > li:nth-child(1)').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2)').text() || + '' + + time = time + .replace(/\[.*\]/, '') + .replace('مساءً', 'PM') + .replace('صباحًا', 'AM') + .trim() + + time = `${initDate.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD hh:mm A', dayjs.tz.guess()) +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return ( + $('.row > div.columns.small-6.large-3 > ul > li:nth-child(1) > a').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(1)').text() || + null + ) +} + +function parseDescription(item) { + const $ = cheerio.load(item) + const excerpt = $('.row > div.columns.small-12.large-6 > ul > li:nth-child(3)').text() || '' + + return excerpt.replace('...اقرأ المزيد', '').replace('...Read more', '') +} + +function parseItems(content, channel, date) { + const $ = cheerio.load(content) + + const dateString = date.locale(channel.lang).format('dddd D') + + const list = $('.dates') + .filter((i, el) => { + let parsedDateString = $(el).text().trim() + parsedDateString = parsedDateString.replace(/\s\s+/g, ' ') + + return parsedDateString.includes(dateString) + }) + .first() + .parent() + .next() + + return $('.padded-half', list).toArray() +} diff --git a/sites/elcinema.com/elcinema.com.test.js b/sites/elcinema.com/elcinema.com.test.js index d7ea6e939..3e8e14f7f 100644 --- a/sites/elcinema.com/elcinema.com.test.js +++ b/sites/elcinema.com/elcinema.com.test.js @@ -1,69 +1,69 @@ -// npm run grab -- --site=elcinema.com - -const { parser, url } = require('./elcinema.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channelAR = { - lang: 'ar', - site_id: '1254', - xmltv_id: 'OSNSeries.ae' -} -const channelEN = { - lang: 'en', - site_id: '1254', - xmltv_id: 'OSNSeries.ae' -} - -it('can generate valid url', () => { - expect(url({ channel: channelEN })).toBe('https://elcinema.com/en/tvguide/1254/') -}) - -it('can parse response (en)', () => { - const contentEN = fs.readFileSync(path.resolve(__dirname, '__data__/content.en.html')) - const results = parser({ date, channel: channelEN, content: contentEN }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-27T14:25:00.000Z', - stop: '2022-08-27T15:15:00.000Z', - title: 'Station 19 S5', - icon: 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', - category: 'Series' - }) -}) - -it('can parse response (ar)', () => { - const contentAR = fs.readFileSync(path.resolve(__dirname, '__data__/content.ar.html')) - const results = parser({ date, channel: channelAR, content: contentAR }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-27T14:25:00.000Z', - stop: '2022-08-27T15:15:00.000Z', - title: 'Station 19 S5', - icon: 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', - category: 'مسلسل' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel: channelEN, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=elcinema.com + +const { parser, url } = require('./elcinema.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channelAR = { + lang: 'ar', + site_id: '1254', + xmltv_id: 'OSNSeries.ae' +} +const channelEN = { + lang: 'en', + site_id: '1254', + xmltv_id: 'OSNSeries.ae' +} + +it('can generate valid url', () => { + expect(url({ channel: channelEN })).toBe('https://elcinema.com/en/tvguide/1254/') +}) + +it('can parse response (en)', () => { + const contentEN = fs.readFileSync(path.resolve(__dirname, '__data__/content.en.html')) + const results = parser({ date, channel: channelEN, content: contentEN }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-27T14:25:00.000Z', + stop: '2022-08-27T15:15:00.000Z', + title: 'Station 19 S5', + icon: 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', + category: 'Series' + }) +}) + +it('can parse response (ar)', () => { + const contentAR = fs.readFileSync(path.resolve(__dirname, '__data__/content.ar.html')) + const results = parser({ date, channel: channelAR, content: contentAR }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-27T14:25:00.000Z', + stop: '2022-08-27T15:15:00.000Z', + title: 'Station 19 S5', + icon: 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', + category: 'مسلسل' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel: channelEN, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js index d8401689d..e94f170a1 100644 --- a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js +++ b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js @@ -1,68 +1,68 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'ena.skylifetv.co.kr', - days: 2, - url({ channel, date }) { - return `http://ena.skylifetv.co.kr/${channel.site_id}/?day=${date.format('YYYYMMDD')}&sc_dvsn=U` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.col2 > .tit').text().trim() -} - -function parseRating($item) { - const rating = $item('.col4').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseDuration($item) { - const duration = $item('.col5').text().trim() - - return duration ? parseInt(duration) : 30 -} - -function parseStart($item, date) { - const time = $item('.col1').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.tbl_schedule > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'ena.skylifetv.co.kr', + days: 2, + url({ channel, date }) { + return `http://ena.skylifetv.co.kr/${channel.site_id}/?day=${date.format('YYYYMMDD')}&sc_dvsn=U` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.col2 > .tit').text().trim() +} + +function parseRating($item) { + const rating = $item('.col4').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseDuration($item) { + const duration = $item('.col5').text().trim() + + return duration ? parseInt(duration) : 30 +} + +function parseStart($item, date) { + const time = $item('.col1').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.tbl_schedule > tbody > tr').toArray() +} diff --git a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js index 12c9255cb..78728b687 100644 --- a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js +++ b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js @@ -1,59 +1,59 @@ -// npm run grab -- --site=ena.skylifetv.co.kr - -const { parser, url } = require('./ena.skylifetv.co.kr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ENA', - xmltv_id: 'ENA.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('http://ena.skylifetv.co.kr/ENA/?day=20230127&sc_dvsn=U') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-26T16:05:00.000Z', - stop: '2023-01-26T17:20:00.000Z', - title: '법쩐 6화', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[17]).toMatchObject({ - start: '2023-01-27T14:10:00.000Z', - stop: '2023-01-27T15:25:00.000Z', - title: '남이 될 수 있을까 4화', - rating: { - system: 'KMRB', - value: '15' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=ena.skylifetv.co.kr + +const { parser, url } = require('./ena.skylifetv.co.kr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ENA', + xmltv_id: 'ENA.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('http://ena.skylifetv.co.kr/ENA/?day=20230127&sc_dvsn=U') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-26T16:05:00.000Z', + stop: '2023-01-26T17:20:00.000Z', + title: '법쩐 6화', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[17]).toMatchObject({ + start: '2023-01-27T14:10:00.000Z', + stop: '2023-01-27T15:25:00.000Z', + title: '남이 될 수 있을까 4화', + rating: { + system: 'KMRB', + value: '15' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/entertainment.ie/entertainment.ie.config.js b/sites/entertainment.ie/entertainment.ie.config.js index 0ac331d11..65df3b865 100644 --- a/sites/entertainment.ie/entertainment.ie.config.js +++ b/sites/entertainment.ie/entertainment.ie.config.js @@ -1,95 +1,95 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'entertainment.ie', - days: 2, - url: function ({ date, channel }) { - return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format( - 'DD-MM-YYYY' - )}&time=all-day` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - } - const duration = parseDuration($item) - const stop = start.plus({ minutes: duration }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - categories: parseCategories($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://entertainment.ie/tv/all-channels/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - let channels = $('.tv-filter-container > tv-filter').attr(':channels') - channels = JSON.parse(channels) - - return channels.map(c => { - return { - site_id: c.slug, - name: c.name - } - }) - } -} - -function parseIcon($item) { - return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img') -} - -function parseTitle($item) { - return $item('.text-holder h3').text().trim() -} - -function parseDescription($item) { - return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description') -} - -function parseCategories($item) { - const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres') - - return genres ? genres.split(', ') : [] -} - -function parseStart($item, date) { - let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time') - let [, time] = d ? d.split(', ') : [null, null] - - return time - ? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'UTC' - }).toUTC() - : null -} - -function parseDuration($item) { - const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration') - - return parseInt(duration) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.info-list > li').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'entertainment.ie', + days: 2, + url: function ({ date, channel }) { + return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format( + 'DD-MM-YYYY' + )}&time=all-day` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + } + const duration = parseDuration($item) + const stop = start.plus({ minutes: duration }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + categories: parseCategories($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://entertainment.ie/tv/all-channels/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + let channels = $('.tv-filter-container > tv-filter').attr(':channels') + channels = JSON.parse(channels) + + return channels.map(c => { + return { + site_id: c.slug, + name: c.name + } + }) + } +} + +function parseIcon($item) { + return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img') +} + +function parseTitle($item) { + return $item('.text-holder h3').text().trim() +} + +function parseDescription($item) { + return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description') +} + +function parseCategories($item) { + const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres') + + return genres ? genres.split(', ') : [] +} + +function parseStart($item, date) { + let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time') + let [, time] = d ? d.split(', ') : [null, null] + + return time + ? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'UTC' + }).toUTC() + : null +} + +function parseDuration($item) { + const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration') + + return parseInt(duration) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.info-list > li').toArray() +} diff --git a/sites/entertainment.ie/entertainment.ie.test.js b/sites/entertainment.ie/entertainment.ie.test.js index 4f0270987..beb41cdfb 100644 --- a/sites/entertainment.ie/entertainment.ie.test.js +++ b/sites/entertainment.ie/entertainment.ie.test.js @@ -1,59 +1,59 @@ -// npm run channels:parse -- --config=./sites/entertainment.ie/entertainment.ie.config.js --output=./sites/entertainment.ie/entertainment.ie.channels.xml -// npm run grab -- --site=entertainment.ie - -const fs = require('fs') -const path = require('path') -const { parser, url } = require('./entertainment.ie.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-29', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' } - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ date, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(51) - - expect(results[0]).toMatchObject({ - start: '2023-06-29T06:00:00.000Z', - stop: '2023-06-29T08:00:00.000Z', - title: 'EuroNews', - description: 'European and international headlines live via satellite', - icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', - categories: ['Factual'] - }) - - expect(results[50]).toMatchObject({ - start: '2023-06-30T02:25:00.000Z', - stop: '2023-06-30T06:00:00.000Z', - title: 'EuroNews', - description: 'European and international headlines live via satellite', - icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', - categories: ['Factual'] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) - const result = parser({ - date, - channel, - content - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/entertainment.ie/entertainment.ie.config.js --output=./sites/entertainment.ie/entertainment.ie.channels.xml +// npm run grab -- --site=entertainment.ie + +const fs = require('fs') +const path = require('path') +const { parser, url } = require('./entertainment.ie.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-29', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' } + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ date, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(51) + + expect(results[0]).toMatchObject({ + start: '2023-06-29T06:00:00.000Z', + stop: '2023-06-29T08:00:00.000Z', + title: 'EuroNews', + description: 'European and international headlines live via satellite', + icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', + categories: ['Factual'] + }) + + expect(results[50]).toMatchObject({ + start: '2023-06-30T02:25:00.000Z', + stop: '2023-06-30T06:00:00.000Z', + title: 'EuroNews', + description: 'European and international headlines live via satellite', + icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', + categories: ['Factual'] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) + const result = parser({ + date, + channel, + content + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/epg.i-cable.com/epg.i-cable.com.config.js b/sites/epg.i-cable.com/epg.i-cable.com.config.js index 200741746..4a41713ce 100644 --- a/sites/epg.i-cable.com/epg.i-cable.com.config.js +++ b/sites/epg.i-cable.com/epg.i-cable.com.config.js @@ -1,89 +1,89 @@ -const axios = require('axios') -const { DateTime } = require('luxon') - -const API_ENDPOINT = 'http://epg.i-cable.com/ci/channel' - -module.exports = { - site: 'epg.i-cable.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}?api=api` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, date) - for (let item of items) { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - const stop = start.plus({ minutes: 30 }) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - programs.push({ - title: parseTitle(item, channel), - start, - stop - }) - } - - return programs - }, - async channels({ lang }) { - const data = await axios - .get(`${API_ENDPOINT}/category/0?api=api`) - .then(r => r.data) - .catch(console.error) - - let channels = [] - const promises = data.cates.map(c => axios.get(`${API_ENDPOINT}/category/${c.cate_id}?api=api`)) - await Promise.allSettled(promises).then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - channels = channels.concat(r.value.data.chs) - } - }) - }) - - return channels.map(c => { - let name = lang === 'zh' ? c.channel_name : c.channel_name_en - name = c.remark_id == 3 ? `${name} [HD]` : name - - return { - site_id: c.channel_no, - name, - lang - } - }) - } -} - -function parseTitle(item, channel) { - return channel.lang === 'en' ? item.programme_name_eng : item.programme_name_chi -} - -function parseStart(item, date) { - let meridiem = item.session_mark === 'PM' ? 'PM' : 'AM' - return DateTime.fromFormat( - `${date.format('YYYY-MM-DD')} ${item.time} ${meridiem}`, - 'yyyy-MM-dd hh:mm a', - { - zone: 'Asia/Hong_Kong' - } - ).toUTC() -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.epgs)) return [] - - return data.epgs -} +const axios = require('axios') +const { DateTime } = require('luxon') + +const API_ENDPOINT = 'http://epg.i-cable.com/ci/channel' + +module.exports = { + site: 'epg.i-cable.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}?api=api` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, date) + for (let item of items) { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + const stop = start.plus({ minutes: 30 }) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + programs.push({ + title: parseTitle(item, channel), + start, + stop + }) + } + + return programs + }, + async channels({ lang }) { + const data = await axios + .get(`${API_ENDPOINT}/category/0?api=api`) + .then(r => r.data) + .catch(console.error) + + let channels = [] + const promises = data.cates.map(c => axios.get(`${API_ENDPOINT}/category/${c.cate_id}?api=api`)) + await Promise.allSettled(promises).then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + channels = channels.concat(r.value.data.chs) + } + }) + }) + + return channels.map(c => { + let name = lang === 'zh' ? c.channel_name : c.channel_name_en + name = c.remark_id == 3 ? `${name} [HD]` : name + + return { + site_id: c.channel_no, + name, + lang + } + }) + } +} + +function parseTitle(item, channel) { + return channel.lang === 'en' ? item.programme_name_eng : item.programme_name_chi +} + +function parseStart(item, date) { + let meridiem = item.session_mark === 'PM' ? 'PM' : 'AM' + return DateTime.fromFormat( + `${date.format('YYYY-MM-DD')} ${item.time} ${meridiem}`, + 'yyyy-MM-dd hh:mm a', + { + zone: 'Asia/Hong_Kong' + } + ).toUTC() +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.epgs)) return [] + + return data.epgs +} diff --git a/sites/epg.i-cable.com/epg.i-cable.com.test.js b/sites/epg.i-cable.com/epg.i-cable.com.test.js index 8fc59b982..1982ce16a 100644 --- a/sites/epg.i-cable.com/epg.i-cable.com.test.js +++ b/sites/epg.i-cable.com/epg.i-cable.com.test.js @@ -1,73 +1,73 @@ -// npm run channels:parse -- --config=./sites/epg.i-cable.com/epg.i-cable.com.config.js --output=./sites/epg.i-cable.com/epg.i-cable.com.channels.xml --set=lang:zh -// npm run grab -- --site=epg.i-cable.com - -const { parser, url } = require('./epg.i-cable.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '003', - xmltv_id: 'HOYTV.hk', - lang: 'zh' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'http://epg.i-cable.com/ci/channel/epg/003/2022-11-15?api=api' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: 'Bloomberg 時段' - }) - - expect(results[31]).toMatchObject({ - start: '2022-11-15T21:00:00.000Z', - stop: '2022-11-15T21:30:00.000Z', - title: 'Bloomberg 時段' - }) -}) - -it('can parse response in English', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const channelEN = { ...channel, lang: 'en' } - let results = parser({ content, channel: channelEN, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: 'Bloomberg Hour' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ date, channel, content }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/epg.i-cable.com/epg.i-cable.com.config.js --output=./sites/epg.i-cable.com/epg.i-cable.com.channels.xml --set=lang:zh +// npm run grab -- --site=epg.i-cable.com + +const { parser, url } = require('./epg.i-cable.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '003', + xmltv_id: 'HOYTV.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'http://epg.i-cable.com/ci/channel/epg/003/2022-11-15?api=api' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: 'Bloomberg 時段' + }) + + expect(results[31]).toMatchObject({ + start: '2022-11-15T21:00:00.000Z', + stop: '2022-11-15T21:30:00.000Z', + title: 'Bloomberg 時段' + }) +}) + +it('can parse response in English', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const channelEN = { ...channel, lang: 'en' } + let results = parser({ content, channel: channelEN, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: 'Bloomberg Hour' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ date, channel, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/firstmedia.com/firstmedia.com.config.js b/sites/firstmedia.com/firstmedia.com.config.js index 75237b1e2..ec62c1a62 100644 --- a/sites/firstmedia.com/firstmedia.com.config.js +++ b/sites/firstmedia.com/firstmedia.com.config.js @@ -1,52 +1,52 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'firstmedia.com', - days: 1, - url: function ({ channel, date }) { - return `https://www.firstmedia.com/ajax/schedule?date=${date.format('DD/MM/YYYY')}&channel=${ - channel.site_id - }&start_time=1&end_time=24&need_channels=0` - }, - parser: function ({ content, channel }) { - if (!content || !channel) return [] - - let programs = [] - const items = parseItems(content, channel.site_id) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - start: parseStart(item).toISOString(), - stop: parseStop(item).toISOString() - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - return JSON.parse(content.trim()).entries[channel] -} - -function parseTitle(item) { - return item.title -} - -function parseDescription(item) { - return item.long_description -} - -function parseStart(item) { - return dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') -} - -function parseStop(item) { - return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'firstmedia.com', + days: 1, + url: function ({ channel, date }) { + return `https://www.firstmedia.com/ajax/schedule?date=${date.format('DD/MM/YYYY')}&channel=${ + channel.site_id + }&start_time=1&end_time=24&need_channels=0` + }, + parser: function ({ content, channel }) { + if (!content || !channel) return [] + + let programs = [] + const items = parseItems(content, channel.site_id) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + start: parseStart(item).toISOString(), + stop: parseStop(item).toISOString() + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + return JSON.parse(content.trim()).entries[channel] +} + +function parseTitle(item) { + return item.title +} + +function parseDescription(item) { + return item.long_description +} + +function parseStart(item) { + return dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') +} + +function parseStop(item) { + return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') +} diff --git a/sites/firstmedia.com/firstmedia.com.test.js b/sites/firstmedia.com/firstmedia.com.test.js index 821fdaac8..9e717c74b 100644 --- a/sites/firstmedia.com/firstmedia.com.test.js +++ b/sites/firstmedia.com/firstmedia.com.test.js @@ -1,35 +1,35 @@ -const { url, parser } = require('./firstmedia.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-18', 'DD/MM/YYYY').startOf('d') -const channel = { site_id: '251', xmltv_id: 'ABCAustralia.au', lang: 'id' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.firstmedia.com/ajax/schedule?date=18/06/2023&channel=251&start_time=1&end_time=24&need_channels=0' - ) -}) - -it('can parse response', () => { - const content = - '{"entries":{"251":[{"logo":"files/images/d/new-logo/channels/11-NEWS/ABC Australia SD-FirstMedia-Chl-251.jpg","name":"ABC Australia","id":"2a800e8a-fdcc-47b3-a4a6-58d1d122b326","channel_id":"a1840c59-6c92-8233-3a02-230246aae0c4","channel_no":251,"programme_id":null,"episode":null,"title":"China Tonight","slug":null,"date":"2023-06-13 00:00:00","start_time":"2023-06-13 10:55:00","end_time":"2023-06-13 11:30:00","length":2100,"description":"China Tonight","long_description":"China is a superpower that dominates global news but it\'s also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today.","status":"0","created_by":null,"updated_by":null,"created_at":"2023-06-13 00:20:24","updated_at":"2023-06-13 00:20:24"}]}}' - const results = parser({ content, channel }) - - expect(results).toMatchObject([ - { - start: '2023-06-13T03:55:00.000Z', - stop: '2023-06-13T04:30:00.000Z', - title: 'China Tonight', - description: - "China is a superpower that dominates global news but it's also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today." - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { url, parser } = require('./firstmedia.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-18', 'DD/MM/YYYY').startOf('d') +const channel = { site_id: '251', xmltv_id: 'ABCAustralia.au', lang: 'id' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.firstmedia.com/ajax/schedule?date=18/06/2023&channel=251&start_time=1&end_time=24&need_channels=0' + ) +}) + +it('can parse response', () => { + const content = + '{"entries":{"251":[{"logo":"files/images/d/new-logo/channels/11-NEWS/ABC Australia SD-FirstMedia-Chl-251.jpg","name":"ABC Australia","id":"2a800e8a-fdcc-47b3-a4a6-58d1d122b326","channel_id":"a1840c59-6c92-8233-3a02-230246aae0c4","channel_no":251,"programme_id":null,"episode":null,"title":"China Tonight","slug":null,"date":"2023-06-13 00:00:00","start_time":"2023-06-13 10:55:00","end_time":"2023-06-13 11:30:00","length":2100,"description":"China Tonight","long_description":"China is a superpower that dominates global news but it\'s also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today.","status":"0","created_by":null,"updated_by":null,"created_at":"2023-06-13 00:20:24","updated_at":"2023-06-13 00:20:24"}]}}' + const results = parser({ content, channel }) + + expect(results).toMatchObject([ + { + start: '2023-06-13T03:55:00.000Z', + stop: '2023-06-13T04:30:00.000Z', + title: 'China Tonight', + description: + "China is a superpower that dominates global news but it's also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today." + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/flixed.io/flixed.io.config.js b/sites/flixed.io/flixed.io.config.js index 5dfb5e0ab..315130977 100644 --- a/sites/flixed.io/flixed.io.config.js +++ b/sites/flixed.io/flixed.io.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'flixed.io', - days: 1, // NOTE: changing the date in a request does not change the response - url: function ({ date, channel }) { - return `https://tv-guide.vercel.app/api/stationAirings?stationId=${ - channel.site_id - }&startDateTime=${date.toJSON()}` - }, - parser({ content }) { - let programs = [] - let items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.program.title, - description: item.program.longDescription, - category: item.program.subType, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseIcon(item) { - const uri = item.program.preferredImage.uri - - return uri ? `https://adma.tmsimg.com/assets/${uri}` : null -} - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseItems(content) { - return JSON.parse(content) -} +const dayjs = require('dayjs') + +module.exports = { + site: 'flixed.io', + days: 1, // NOTE: changing the date in a request does not change the response + url: function ({ date, channel }) { + return `https://tv-guide.vercel.app/api/stationAirings?stationId=${ + channel.site_id + }&startDateTime=${date.toJSON()}` + }, + parser({ content }) { + let programs = [] + let items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.program.title, + description: item.program.longDescription, + category: item.program.subType, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseIcon(item) { + const uri = item.program.preferredImage.uri + + return uri ? `https://adma.tmsimg.com/assets/${uri}` : null +} + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseItems(content) { + return JSON.parse(content) +} diff --git a/sites/flixed.io/flixed.io.test.js b/sites/flixed.io/flixed.io.test.js index e22d4b15f..14888ec3e 100644 --- a/sites/flixed.io/flixed.io.test.js +++ b/sites/flixed.io/flixed.io.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=flixed.io - -const { parser, url } = require('./flixed.io.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '108970', - xmltv_id: 'VSiN.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://tv-guide.vercel.app/api/stationAirings?stationId=108970&startDateTime=2023-01-19T00:00:00.000Z' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-19T05:00:00.000Z', - stop: '2023-01-19T06:00:00.000Z', - title: 'The Greg Peterson Experience', - category: 'Sports non-event', - icon: 'https://adma.tmsimg.com/assets/assets/p20628892_b_v13_aa.jpg?w=270&h=360', - description: 'A different kind of sports betting.' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '[]' - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=flixed.io + +const { parser, url } = require('./flixed.io.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '108970', + xmltv_id: 'VSiN.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://tv-guide.vercel.app/api/stationAirings?stationId=108970&startDateTime=2023-01-19T00:00:00.000Z' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-19T05:00:00.000Z', + stop: '2023-01-19T06:00:00.000Z', + title: 'The Greg Peterson Experience', + category: 'Sports non-event', + icon: 'https://adma.tmsimg.com/assets/assets/p20628892_b_v13_aa.jpg?w=270&h=360', + description: 'A different kind of sports betting.' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '[]' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/foxsports.com.au/foxsports.com.au.config.js b/sites/foxsports.com.au/foxsports.com.au.config.js index ec4d346b3..4eba7c47f 100644 --- a/sites/foxsports.com.au/foxsports.com.au.config.js +++ b/sites/foxsports.com.au/foxsports.com.au.config.js @@ -1,42 +1,42 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'foxsports.com.au', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${date.format( - 'YYYY-MM-DD' - )}&to=${date.add(1, 'd').format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.programmeTitle, - sub_title: item.title, - category: item.genreTitle, - description: item.synopsis, - start: dayjs.utc(item.startTime), - stop: dayjs.utc(item.endTime) - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - const programmes = data['channel-programme'] - if (!Array.isArray(programmes)) return [] - - const channelData = programmes.filter(i => i.channelId == channel.site_id) - return channelData && Array.isArray(channelData) ? channelData : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'foxsports.com.au', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${date.format( + 'YYYY-MM-DD' + )}&to=${date.add(1, 'd').format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.programmeTitle, + sub_title: item.title, + category: item.genreTitle, + description: item.synopsis, + start: dayjs.utc(item.startTime), + stop: dayjs.utc(item.endTime) + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + const programmes = data['channel-programme'] + if (!Array.isArray(programmes)) return [] + + const channelData = programmes.filter(i => i.channelId == channel.site_id) + return channelData && Array.isArray(channelData) ? channelData : [] +} diff --git a/sites/foxsports.com.au/foxsports.com.au.test.js b/sites/foxsports.com.au/foxsports.com.au.test.js index 52090f733..8025d55bc 100644 --- a/sites/foxsports.com.au/foxsports.com.au.test.js +++ b/sites/foxsports.com.au/foxsports.com.au.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=foxsports.com.au - -const { parser, url } = require('./foxsports.com.au.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'FoxLeague.au' -} -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15' - ) -}) - -it('can parse response', () => { - const content = - '{"channel-programme":[{"id":"31cc8b4c-3711-49f0-bf22-2ec3993b0a07","programmeTitle":"NRL","title":"Eels v Titans","startTime":"2022-12-14T00:00:00+11:00","endTime":"2022-12-14T01:00:00+11:00","duration":60,"live":false,"genreId":"5c389cf4-8db7-4b52-9773-52355bd28559","channelId":2,"channelName":"FOX League","channelAbbreviation":"LEAGUE","programmeUID":235220,"round":"R1","statsMatchId":null,"closedCaptioned":true,"statsFixtureId":10207,"genreTitle":"Rugby League","parentGenreId":"a953f929-2d12-41a4-b0e9-97f401afff11","parentGenreTitle":"Sport","pmgId":"PMG01306944","statsSport":"league","type":"GAME","hiDef":true,"widescreen":true,"classification":"","synopsis":"The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.","preGameStartTime":null,"closeCaptioned":true}]}' - - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'NRL', - sub_title: 'Eels v Titans', - description: - 'The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.', - category: 'Rugby League', - start: '2022-12-13T13:00:00.000Z', - stop: '2022-12-13T14:00:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser( - { - content: '{"channel-programme":[]}' - }, - channel - ) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=foxsports.com.au + +const { parser, url } = require('./foxsports.com.au.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'FoxLeague.au' +} +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15' + ) +}) + +it('can parse response', () => { + const content = + '{"channel-programme":[{"id":"31cc8b4c-3711-49f0-bf22-2ec3993b0a07","programmeTitle":"NRL","title":"Eels v Titans","startTime":"2022-12-14T00:00:00+11:00","endTime":"2022-12-14T01:00:00+11:00","duration":60,"live":false,"genreId":"5c389cf4-8db7-4b52-9773-52355bd28559","channelId":2,"channelName":"FOX League","channelAbbreviation":"LEAGUE","programmeUID":235220,"round":"R1","statsMatchId":null,"closedCaptioned":true,"statsFixtureId":10207,"genreTitle":"Rugby League","parentGenreId":"a953f929-2d12-41a4-b0e9-97f401afff11","parentGenreTitle":"Sport","pmgId":"PMG01306944","statsSport":"league","type":"GAME","hiDef":true,"widescreen":true,"classification":"","synopsis":"The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.","preGameStartTime":null,"closeCaptioned":true}]}' + + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'NRL', + sub_title: 'Eels v Titans', + description: + 'The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.', + category: 'Rugby League', + start: '2022-12-13T13:00:00.000Z', + stop: '2022-12-13T14:00:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser( + { + content: '{"channel-programme":[]}' + }, + channel + ) + expect(result).toMatchObject([]) +}) diff --git a/sites/foxtel.com.au/foxtel.com.au.config.js b/sites/foxtel.com.au/foxtel.com.au.config.js index e86742569..97ee35903 100644 --- a/sites/foxtel.com.au/foxtel.com.au.config.js +++ b/sites/foxtel.com.au/foxtel.com.au.config.js @@ -1,134 +1,134 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') - -module.exports = { - site: 'foxtel.com.au', - days: 2, - url({ channel, date }) { - return `https://www.foxtel.com.au/tv-guide/channel/${channel.site_id}/${date.format( - 'YYYY/MM/DD' - )}` - }, - request: { - headers: { - 'Accept-Language': 'en-US,en;', - Cookie: 'AAMC_foxtel_0=REGION|6' - } - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - icon: parseIcon($item), - rating: parseRating($item), - season: parseSeason($item), - episode: parseEpisode($item), - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://www.foxtel.com.au/webepg/ws/foxtel/channels?regionId=8336', { - headers: { - 'Accept-Language': 'en-US,en;', - Cookie: 'AAMC_foxtel_0=REGION|6' - } - }) - .then(r => r.data) - .catch(console.log) - - return data.channels.forEach(item => { - let name = item.name.replace(/\+/g, '-').replace(/&/g, '') - const slug = name.replace(/[^a-z0-9\s]/gi, '').replace(/[^a-z0-9]/i, '-') - - return { - name: item.name.replace(/&/g, '&'), - site_id: `${slug}/${item.channelTag}` - } - }) - } -} - -function parseSeason($item) { - let seasonString = $item('.epg-event-description > div > abbr:nth-child(1)').attr('title') - if (!seasonString) return null - let [, season] = seasonString.match(/^Season: (\d+)/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - let episodeString = $item('.epg-event-description > div > abbr:nth-child(2)').attr('title') - if (!episodeString) return null - let [, episode] = episodeString.match(/^Episode: (\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseIcon($item) { - return $item('.epg-event-thumbnail > img').attr('src') -} - -function parseTitle($item) { - return $item('.epg-event-description').clone().children().remove().end().text().trim() -} - -function parseSubTitle($item) { - let subtitle = $item('.epg-event-description > div') - .clone() - .children() - .remove() - .end() - .text() - .trim() - .split(',') - - subtitle = subtitle.pop() - const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] - - return subtitle.replace(`(${rating})`, '').trim() -} - -function parseRating($item) { - const subtitle = $item('.epg-event-description > div').text().trim() - const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] - - return rating - ? { - system: 'ACB', - value: rating - } - : null -} - -function parseStart($item) { - const unix = $item('*').data('scheduled-date') - - return dayjs(parseInt(unix)) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('#epg-channel-events > a').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') + +module.exports = { + site: 'foxtel.com.au', + days: 2, + url({ channel, date }) { + return `https://www.foxtel.com.au/tv-guide/channel/${channel.site_id}/${date.format( + 'YYYY/MM/DD' + )}` + }, + request: { + headers: { + 'Accept-Language': 'en-US,en;', + Cookie: 'AAMC_foxtel_0=REGION|6' + } + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + icon: parseIcon($item), + rating: parseRating($item), + season: parseSeason($item), + episode: parseEpisode($item), + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://www.foxtel.com.au/webepg/ws/foxtel/channels?regionId=8336', { + headers: { + 'Accept-Language': 'en-US,en;', + Cookie: 'AAMC_foxtel_0=REGION|6' + } + }) + .then(r => r.data) + .catch(console.log) + + return data.channels.forEach(item => { + let name = item.name.replace(/\+/g, '-').replace(/&/g, '') + const slug = name.replace(/[^a-z0-9\s]/gi, '').replace(/[^a-z0-9]/i, '-') + + return { + name: item.name.replace(/&/g, '&'), + site_id: `${slug}/${item.channelTag}` + } + }) + } +} + +function parseSeason($item) { + let seasonString = $item('.epg-event-description > div > abbr:nth-child(1)').attr('title') + if (!seasonString) return null + let [, season] = seasonString.match(/^Season: (\d+)/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + let episodeString = $item('.epg-event-description > div > abbr:nth-child(2)').attr('title') + if (!episodeString) return null + let [, episode] = episodeString.match(/^Episode: (\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseIcon($item) { + return $item('.epg-event-thumbnail > img').attr('src') +} + +function parseTitle($item) { + return $item('.epg-event-description').clone().children().remove().end().text().trim() +} + +function parseSubTitle($item) { + let subtitle = $item('.epg-event-description > div') + .clone() + .children() + .remove() + .end() + .text() + .trim() + .split(',') + + subtitle = subtitle.pop() + const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] + + return subtitle.replace(`(${rating})`, '').trim() +} + +function parseRating($item) { + const subtitle = $item('.epg-event-description > div').text().trim() + const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] + + return rating + ? { + system: 'ACB', + value: rating + } + : null +} + +function parseStart($item) { + const unix = $item('*').data('scheduled-date') + + return dayjs(parseInt(unix)) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('#epg-channel-events > a').toArray() +} diff --git a/sites/foxtel.com.au/foxtel.com.au.test.js b/sites/foxtel.com.au/foxtel.com.au.test.js index a3779b7f6..35394654b 100644 --- a/sites/foxtel.com.au/foxtel.com.au.test.js +++ b/sites/foxtel.com.au/foxtel.com.au.test.js @@ -1,62 +1,62 @@ -// npm run channels:parse -- --config=./sites/foxtel.com.au/foxtel.com.au.config.js --output=./sites/foxtel.com.au/foxtel.com.au.channels.xml -// npm run grab -- --site=foxtel.com.au - -const { parser, url, request } = require('./foxtel.com.au.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Channel-9-Sydney/NIN', - xmltv_id: 'Channel9Sydney.au' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.foxtel.com.au/tv-guide/channel/Channel-9-Sydney/NIN/2022/11/08' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Accept-Language': 'en-US,en;', - Cookie: 'AAMC_foxtel_0=REGION|6' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-07T12:40:00.000Z', - stop: '2022-11-07T13:30:00.000Z', - title: 'The Equalizer', - sub_title: 'Glory', - icon: 'https://images1.resources.foxtel.com.au/store2/mount1/16/3/69e0v.jpg?maxheight=90&limit=91aa1c7a2c485aeeba0706941f79f111adb35830', - rating: { - system: 'ACB', - value: 'M' - }, - season: 1, - episode: 2 - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/foxtel.com.au/foxtel.com.au.config.js --output=./sites/foxtel.com.au/foxtel.com.au.channels.xml +// npm run grab -- --site=foxtel.com.au + +const { parser, url, request } = require('./foxtel.com.au.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Channel-9-Sydney/NIN', + xmltv_id: 'Channel9Sydney.au' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.foxtel.com.au/tv-guide/channel/Channel-9-Sydney/NIN/2022/11/08' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Accept-Language': 'en-US,en;', + Cookie: 'AAMC_foxtel_0=REGION|6' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-07T12:40:00.000Z', + stop: '2022-11-07T13:30:00.000Z', + title: 'The Equalizer', + sub_title: 'Glory', + icon: 'https://images1.resources.foxtel.com.au/store2/mount1/16/3/69e0v.jpg?maxheight=90&limit=91aa1c7a2c485aeeba0706941f79f111adb35830', + rating: { + system: 'ACB', + value: 'M' + }, + season: 1, + episode: 2 + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/frikanalen.no/frikanalen.no.config.js b/sites/frikanalen.no/frikanalen.no.config.js index e66a6efe0..a5886752f 100644 --- a/sites/frikanalen.no/frikanalen.no.config.js +++ b/sites/frikanalen.no/frikanalen.no.config.js @@ -1,52 +1,52 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'frikanalen.no', - days: 2, - url({ date }) { - return `https://frikanalen.no/api/scheduleitems/?date=${date.format( - 'YYYY-MM-DD' - )}&format=json&limit=100` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - category: parseCategory(item), - description: parseDescription(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseTitle(item) { - return item.video.name -} - -function parseCategory(item) { - return item.video.categories -} - -function parseDescription(item) { - return item.video.header -} - -function parseStart(item) { - return dayjs(item.starttime) -} - -function parseStop(item) { - return dayjs(item.endtime) -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data && Array.isArray(data.results) ? data.results : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'frikanalen.no', + days: 2, + url({ date }) { + return `https://frikanalen.no/api/scheduleitems/?date=${date.format( + 'YYYY-MM-DD' + )}&format=json&limit=100` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + category: parseCategory(item), + description: parseDescription(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseTitle(item) { + return item.video.name +} + +function parseCategory(item) { + return item.video.categories +} + +function parseDescription(item) { + return item.video.header +} + +function parseStart(item) { + return dayjs(item.starttime) +} + +function parseStop(item) { + return dayjs(item.endtime) +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data && Array.isArray(data.results) ? data.results : [] +} diff --git a/sites/frikanalen.no/frikanalen.no.test.js b/sites/frikanalen.no/frikanalen.no.test.js index 8c9fb2da5..37a249030 100644 --- a/sites/frikanalen.no/frikanalen.no.test.js +++ b/sites/frikanalen.no/frikanalen.no.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=frikanalen.no - -const { parser, url } = require('./frikanalen.no.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'Frikanalen.no' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://frikanalen.no/api/scheduleitems/?date=2022-01-19&format=json&limit=100' - ) -}) - -it('can parse response', () => { - const content = - '{"count":83,"next":null,"previous":null,"results":[{"id":135605,"video":{"id":626094,"name":"FSCONS 2017 - Keynote: TBA - Linda Sandvik","header":"Linda Sandvik\'s keynote at FSCONS 2017\\r\\n\\r\\nRecorded by NUUG for FSCONS.","description":null,"creator":"davidwnoble@gmail.com","organization":{"id":82,"name":"NUUG","homepage":"https://www.nuug.no/","description":"Forening NUUG er for alle som er interessert i fri programvare, åpne standarder og Unix-lignende operativsystemer.","postalAddress":"","streetAddress":"","editorId":2148,"editorName":"David Noble","editorEmail":"davidwnoble@gmail.com","editorMsisdn":"","fkmember":true},"duration":"00:57:55.640000","categories":["Samfunn"]},"schedulereason":5,"starttime":"2022-01-19T00:47:00+01:00","endtime":"2022-01-19T01:44:55.640000+01:00","duration":"00:57:55.640000"}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-18T23:47:00.000Z', - stop: '2022-01-19T00:44:55.640Z', - title: 'FSCONS 2017 - Keynote: TBA - Linda Sandvik', - category: ['Samfunn'], - description: "Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS." - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"count":0,"next":null,"previous":null,"results":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=frikanalen.no + +const { parser, url } = require('./frikanalen.no.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'Frikanalen.no' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://frikanalen.no/api/scheduleitems/?date=2022-01-19&format=json&limit=100' + ) +}) + +it('can parse response', () => { + const content = + '{"count":83,"next":null,"previous":null,"results":[{"id":135605,"video":{"id":626094,"name":"FSCONS 2017 - Keynote: TBA - Linda Sandvik","header":"Linda Sandvik\'s keynote at FSCONS 2017\\r\\n\\r\\nRecorded by NUUG for FSCONS.","description":null,"creator":"davidwnoble@gmail.com","organization":{"id":82,"name":"NUUG","homepage":"https://www.nuug.no/","description":"Forening NUUG er for alle som er interessert i fri programvare, åpne standarder og Unix-lignende operativsystemer.","postalAddress":"","streetAddress":"","editorId":2148,"editorName":"David Noble","editorEmail":"davidwnoble@gmail.com","editorMsisdn":"","fkmember":true},"duration":"00:57:55.640000","categories":["Samfunn"]},"schedulereason":5,"starttime":"2022-01-19T00:47:00+01:00","endtime":"2022-01-19T01:44:55.640000+01:00","duration":"00:57:55.640000"}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-18T23:47:00.000Z', + stop: '2022-01-19T00:44:55.640Z', + title: 'FSCONS 2017 - Keynote: TBA - Linda Sandvik', + category: ['Samfunn'], + description: "Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS." + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"count":0,"next":null,"previous":null,"results":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/gatotv.com/gatotv.com.config.js b/sites/gatotv.com/gatotv.com.config.js index 6c7a235ee..1ace7d5bf 100644 --- a/sites/gatotv.com/gatotv.com.config.js +++ b/sites/gatotv.com/gatotv.com.config.js @@ -1,100 +1,100 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const url = require('url') -const path = require('path') -const { DateTime } = require('luxon') - -module.exports = { - site: 'gatotv.com', - days: 2, - url({ channel, date }) { - return `https://www.gatotv.com/canal/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - date = date.subtract(1, 'd') - items.forEach((item, i) => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (i === 0 && start.hour >= 5) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.gatotv.com/guia_tv/completa') - .then(response => response.data) - .catch(console.log) - - const $ = cheerio.load(data) - const items = $('.tbl_EPG_row,.tbl_EPG_rowAlternate').toArray() - - return items.map(item => { - const $item = cheerio.load(item) - const link = $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').attr('href') - const parsed = url.parse(link) - - return { - lang: 'es', - site_id: path.basename(parsed.pathname), - name: $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').text() - } - }) - } -} - -function parseTitle($item) { - return $item('td:nth-child(4) > div > div > a > span,td:nth-child(3) > div > div > span,td:nth-child(3) > div > div > a > span').text() -} - -function parseDescription($item) { - return $item('td:nth-child(4) > div').clone().children().remove().end().text().trim() -} - -function parseIcon($item) { - return $item('td:nth-child(3) > a > img').attr('src') -} - -function parseStart($item, date) { - const time = $item('td:nth-child(1) > div > time').attr('datetime') - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'EST' - }).toUTC() -} - -function parseStop($item, date) { - const time = $item('td:nth-child(2) > div > time').attr('datetime') - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'EST' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - 'body > div.div_content > table:nth-child(8) > tbody > tr:nth-child(2) > td:nth-child(1) > table.tbl_EPG' - ) - .find('.tbl_EPG_row,.tbl_EPG_rowAlternate,.tbl_EPG_row_selected') - .toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const url = require('url') +const path = require('path') +const { DateTime } = require('luxon') + +module.exports = { + site: 'gatotv.com', + days: 2, + url({ channel, date }) { + return `https://www.gatotv.com/canal/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + date = date.subtract(1, 'd') + items.forEach((item, i) => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (i === 0 && start.hour >= 5) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.gatotv.com/guia_tv/completa') + .then(response => response.data) + .catch(console.log) + + const $ = cheerio.load(data) + const items = $('.tbl_EPG_row,.tbl_EPG_rowAlternate').toArray() + + return items.map(item => { + const $item = cheerio.load(item) + const link = $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').attr('href') + const parsed = url.parse(link) + + return { + lang: 'es', + site_id: path.basename(parsed.pathname), + name: $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').text() + } + }) + } +} + +function parseTitle($item) { + return $item('td:nth-child(4) > div > div > a > span,td:nth-child(3) > div > div > span,td:nth-child(3) > div > div > a > span').text() +} + +function parseDescription($item) { + return $item('td:nth-child(4) > div').clone().children().remove().end().text().trim() +} + +function parseIcon($item) { + return $item('td:nth-child(3) > a > img').attr('src') +} + +function parseStart($item, date) { + const time = $item('td:nth-child(1) > div > time').attr('datetime') + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'EST' + }).toUTC() +} + +function parseStop($item, date) { + const time = $item('td:nth-child(2) > div > time').attr('datetime') + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'EST' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + 'body > div.div_content > table:nth-child(8) > tbody > tr:nth-child(2) > td:nth-child(1) > table.tbl_EPG' + ) + .find('.tbl_EPG_row,.tbl_EPG_rowAlternate,.tbl_EPG_row_selected') + .toArray() +} diff --git a/sites/gatotv.com/gatotv.com.test.js b/sites/gatotv.com/gatotv.com.test.js index 5f96e16ee..50c8a572b 100644 --- a/sites/gatotv.com/gatotv.com.test.js +++ b/sites/gatotv.com/gatotv.com.test.js @@ -1,82 +1,82 @@ -// npm run channels:parse -- --config=./sites/gatotv.com/gatotv.com.config.js --output=./sites/gatotv.com/gatotv.com.channels.xml -// npm run grab -- --site=gatotv.com - -const { parser, url } = require('./gatotv.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -let date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'm_0', - xmltv_id: '0porMovistarPlus.es' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.gatotv.com/canal/m_0/2023-06-13') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.html'), 'utf8') - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-13T04:30:00.000Z', - stop: '2023-06-13T05:32:00.000Z', - title: 'Supergarcía' - }) - - expect(results[1]).toMatchObject({ - start: '2023-06-13T05:32:00.000Z', - stop: '2023-06-13T06:59:00.000Z', - title: 'La resistencia' - }) - - expect(results[25]).toMatchObject({ - start: '2023-06-14T04:46:00.000Z', - stop: '2023-06-14T05:00:00.000Z', - title: 'Una familia absolutamente normal' - }) -}) - -it('can parse response when the guide starts from midnight', () => { - date = date.add(1, 'd') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html'), 'utf8') - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-14T05:00:00.000Z', - stop: '2023-06-14T05:32:00.000Z', - title: 'Ilustres Ignorantes' - }) - - expect(results[26]).toMatchObject({ - start: '2023-06-15T04:30:00.000Z', - stop: '2023-06-15T05:30:00.000Z', - title: 'Showriano' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/gatotv.com/gatotv.com.config.js --output=./sites/gatotv.com/gatotv.com.channels.xml +// npm run grab -- --site=gatotv.com + +const { parser, url } = require('./gatotv.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +let date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'm_0', + xmltv_id: '0porMovistarPlus.es' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.gatotv.com/canal/m_0/2023-06-13') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.html'), 'utf8') + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-13T04:30:00.000Z', + stop: '2023-06-13T05:32:00.000Z', + title: 'Supergarcía' + }) + + expect(results[1]).toMatchObject({ + start: '2023-06-13T05:32:00.000Z', + stop: '2023-06-13T06:59:00.000Z', + title: 'La resistencia' + }) + + expect(results[25]).toMatchObject({ + start: '2023-06-14T04:46:00.000Z', + stop: '2023-06-14T05:00:00.000Z', + title: 'Una familia absolutamente normal' + }) +}) + +it('can parse response when the guide starts from midnight', () => { + date = date.add(1, 'd') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html'), 'utf8') + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-14T05:00:00.000Z', + stop: '2023-06-14T05:32:00.000Z', + title: 'Ilustres Ignorantes' + }) + + expect(results[26]).toMatchObject({ + start: '2023-06-15T04:30:00.000Z', + stop: '2023-06-15T05:30:00.000Z', + title: 'Showriano' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/getafteritmedia.com/getafteritmedia.com.config.js b/sites/getafteritmedia.com/getafteritmedia.com.config.js index 5fdcaed40..a86068cbf 100644 --- a/sites/getafteritmedia.com/getafteritmedia.com.config.js +++ b/sites/getafteritmedia.com/getafteritmedia.com.config.js @@ -1,64 +1,64 @@ -const table2array = require('table2array') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const isoWeek = require('dayjs/plugin/isoWeek') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) -dayjs.extend(isoWeek) - -module.exports = { - site: 'getafteritmedia.com', - days: 2, - url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml', - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.time}`, - 'YYYY-MM-DD HH:mm A', - 'America/New_York' - ) -} - -function parseItems(content, channel, date) { - const day = date.isoWeekday() - const $ = cheerio.load(content) - const table = $.html($(`#${channel.site_id} table`)) - let data = table2array(table) - data.splice(0, 5) - - return data.map(row => { - return { - time: row[1], - title: row[day + 1] - } - }) -} +const table2array = require('table2array') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const isoWeek = require('dayjs/plugin/isoWeek') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) +dayjs.extend(isoWeek) + +module.exports = { + site: 'getafteritmedia.com', + days: 2, + url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml', + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.time}`, + 'YYYY-MM-DD HH:mm A', + 'America/New_York' + ) +} + +function parseItems(content, channel, date) { + const day = date.isoWeekday() + const $ = cheerio.load(content) + const table = $.html($(`#${channel.site_id} table`)) + let data = table2array(table) + data.splice(0, 5) + + return data.map(row => { + return { + time: row[1], + title: row[day + 1] + } + }) +} diff --git a/sites/getafteritmedia.com/getafteritmedia.com.test.js b/sites/getafteritmedia.com/getafteritmedia.com.test.js index 5d95345f0..292caf422 100644 --- a/sites/getafteritmedia.com/getafteritmedia.com.test.js +++ b/sites/getafteritmedia.com/getafteritmedia.com.test.js @@ -1,47 +1,47 @@ -// npm run grab -- --site=getafteritmedia.com - -const { parser, url } = require('./getafteritmedia.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '494637005', - xmltv_id: 'REVNWebFeed.us' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-26T05:00:00.000Z', - stop: '2022-11-26T05:30:00.000Z', - title: 'The Appraisers' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=getafteritmedia.com + +const { parser, url } = require('./getafteritmedia.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '494637005', + xmltv_id: 'REVNWebFeed.us' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-26T05:00:00.000Z', + stop: '2022-11-26T05:30:00.000Z', + title: 'The Appraisers' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/guidatv.sky.it/guidatv.sky.it.config.js b/sites/guidatv.sky.it/guidatv.sky.it.config.js index d3f5dcf50..eac77e969 100644 --- a/sites/guidatv.sky.it/guidatv.sky.it.config.js +++ b/sites/guidatv.sky.it/guidatv.sky.it.config.js @@ -1,72 +1,72 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'guidatv.sky.it', - days: 2, - url: function ({ date, channel }) { - const [env, id] = channel.site_id.split('#') - return `https://apid.sky.it/gtv/v1/events?from=${date.format('YYYY-MM-DD')}T00:00:00Z&to=${date - .add(1, 'd') - .format('YYYY-MM-DD')}T00:00:00Z&pageSize=999&pageNum=0&env=${env}&channels=${id}` - }, - parser: function ({ content }) { - const programs = [] - const data = JSON.parse(content) - const items = data.events - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.eventTitle, - description: item.eventSynopsis, - category: parseCategory(item), - season: parseSeason(item), - episode: parseEpisode(item), - start: parseStart(item), - stop: parseStop(item), - url: parseURL(item), - icon: parseIcon(item) - }) - }) - - return programs - } -} - -function parseCategory(item) { - let category = item.content.genre.name || null - const subcategory = item.content.subgenre.name || null - if (category && subcategory) { - category += `/${subcategory}` - } - return category -} - -function parseStart(item) { - return item.starttime ? dayjs(item.starttime) : null -} - -function parseStop(item) { - return item.endtime ? dayjs(item.endtime) : null -} - -function parseURL(item) { - return item.content.url ? `https://guidatv.sky.it${item.content.url}` : null -} - -function parseIcon(item) { - const cover = item.content.imagesMap ? item.content.imagesMap.find(i => i.key === 'cover') : null - - return cover && cover.img && cover.img.url ? `https://guidatv.sky.it${cover.img.url}` : null -} - -function parseSeason(item) { - if (!item.content.seasonNumber) return null - if (String(item.content.seasonNumber).length > 2) return null - return item.content.seasonNumber -} - -function parseEpisode(item) { - if (!item.content.episodeNumber) return null - if (String(item.content.episodeNumber).length > 3) return null - return item.content.episodeNumber -} +const dayjs = require('dayjs') + +module.exports = { + site: 'guidatv.sky.it', + days: 2, + url: function ({ date, channel }) { + const [env, id] = channel.site_id.split('#') + return `https://apid.sky.it/gtv/v1/events?from=${date.format('YYYY-MM-DD')}T00:00:00Z&to=${date + .add(1, 'd') + .format('YYYY-MM-DD')}T00:00:00Z&pageSize=999&pageNum=0&env=${env}&channels=${id}` + }, + parser: function ({ content }) { + const programs = [] + const data = JSON.parse(content) + const items = data.events + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.eventTitle, + description: item.eventSynopsis, + category: parseCategory(item), + season: parseSeason(item), + episode: parseEpisode(item), + start: parseStart(item), + stop: parseStop(item), + url: parseURL(item), + icon: parseIcon(item) + }) + }) + + return programs + } +} + +function parseCategory(item) { + let category = item.content.genre.name || null + const subcategory = item.content.subgenre.name || null + if (category && subcategory) { + category += `/${subcategory}` + } + return category +} + +function parseStart(item) { + return item.starttime ? dayjs(item.starttime) : null +} + +function parseStop(item) { + return item.endtime ? dayjs(item.endtime) : null +} + +function parseURL(item) { + return item.content.url ? `https://guidatv.sky.it${item.content.url}` : null +} + +function parseIcon(item) { + const cover = item.content.imagesMap ? item.content.imagesMap.find(i => i.key === 'cover') : null + + return cover && cover.img && cover.img.url ? `https://guidatv.sky.it${cover.img.url}` : null +} + +function parseSeason(item) { + if (!item.content.seasonNumber) return null + if (String(item.content.seasonNumber).length > 2) return null + return item.content.seasonNumber +} + +function parseEpisode(item) { + if (!item.content.episodeNumber) return null + if (String(item.content.episodeNumber).length > 3) return null + return item.content.episodeNumber +} diff --git a/sites/guidatv.sky.it/guidatv.sky.it.test.js b/sites/guidatv.sky.it/guidatv.sky.it.test.js index 77e5ee237..8ff526ca0 100644 --- a/sites/guidatv.sky.it/guidatv.sky.it.test.js +++ b/sites/guidatv.sky.it/guidatv.sky.it.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=guidatv.sky.it - -const { parser, url } = require('./guidatv.sky.it.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'DTH#10458', - xmltv_id: '20Mediaset.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://apid.sky.it/gtv/v1/events?from=2022-05-06T00:00:00Z&to=2022-05-07T00:00:00Z&pageSize=999&pageNum=0&env=DTH&channels=10458' - ) -}) - -it('can parse response', () => { - const content = - '{"events": [ { "channel": { "id": 10458, "logo": "/logo/545820mediasethd_Light_Fit.png", "logoPadding": "/logo/545820mediasethd_Light_Padding.png", "logoDark": "/logo/545820mediasethd_Dark_Fit.png", "logoDarkPadding": "/logo/545820mediasethd_Dark_Padding.png", "logoLight": "/logo/545820mediasethd_Light_Padding.png", "name": "20Mediaset HD", "number": 151, "category": { "id": 3, "name": "Intrattenimento" } }, "content": { "uuid": "77c630aa-4744-44cb-a88e-3e871c6b73d9", "contentTitle": "Distretto di Polizia", "episodeNumber": 26, "seasonNumber": 6, "url": "/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9", "genre": { "id": 1, "name": "Intrattenimento" }, "subgenre": { "id": 9, "name": "Fiction" }, "imagesMap": [ { "key": "background", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/background?md5ChecksumParam=88d3f48ce855316f4be25ab9bb846d32" } }, { "key": "cover", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b" } }, { "key": "scene", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/16-9?md5ChecksumParam=f41bfe414bec32505abdab19d00b8b43" } } ] }, "eventId": "139585132", "starttime": "2022-05-06T00:35:40Z", "endtime": "2022-05-06T01:15:40Z", "eventTitle": "Distretto di Polizia", "eventSynopsis": "S6 Ep26 La resa dei conti - Fino all\'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e\' tutto come sembrava.", "epgEventTitle": "S6 Ep26 - Distretto di Polizia", "primeVision": false, "resolutions": [ { "resolutionType": "resolution4k", "value": false } ] }]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-05-06T00:35:40.000Z', - stop: '2022-05-06T01:15:40.000Z', - title: 'Distretto di Polizia', - description: - "S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.", - season: 6, - episode: 26, - icon: 'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b', - category: 'Intrattenimento/Fiction', - url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"events":[],"total":0}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=guidatv.sky.it + +const { parser, url } = require('./guidatv.sky.it.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'DTH#10458', + xmltv_id: '20Mediaset.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://apid.sky.it/gtv/v1/events?from=2022-05-06T00:00:00Z&to=2022-05-07T00:00:00Z&pageSize=999&pageNum=0&env=DTH&channels=10458' + ) +}) + +it('can parse response', () => { + const content = + '{"events": [ { "channel": { "id": 10458, "logo": "/logo/545820mediasethd_Light_Fit.png", "logoPadding": "/logo/545820mediasethd_Light_Padding.png", "logoDark": "/logo/545820mediasethd_Dark_Fit.png", "logoDarkPadding": "/logo/545820mediasethd_Dark_Padding.png", "logoLight": "/logo/545820mediasethd_Light_Padding.png", "name": "20Mediaset HD", "number": 151, "category": { "id": 3, "name": "Intrattenimento" } }, "content": { "uuid": "77c630aa-4744-44cb-a88e-3e871c6b73d9", "contentTitle": "Distretto di Polizia", "episodeNumber": 26, "seasonNumber": 6, "url": "/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9", "genre": { "id": 1, "name": "Intrattenimento" }, "subgenre": { "id": 9, "name": "Fiction" }, "imagesMap": [ { "key": "background", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/background?md5ChecksumParam=88d3f48ce855316f4be25ab9bb846d32" } }, { "key": "cover", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b" } }, { "key": "scene", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/16-9?md5ChecksumParam=f41bfe414bec32505abdab19d00b8b43" } } ] }, "eventId": "139585132", "starttime": "2022-05-06T00:35:40Z", "endtime": "2022-05-06T01:15:40Z", "eventTitle": "Distretto di Polizia", "eventSynopsis": "S6 Ep26 La resa dei conti - Fino all\'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e\' tutto come sembrava.", "epgEventTitle": "S6 Ep26 - Distretto di Polizia", "primeVision": false, "resolutions": [ { "resolutionType": "resolution4k", "value": false } ] }]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-05-06T00:35:40.000Z', + stop: '2022-05-06T01:15:40.000Z', + title: 'Distretto di Polizia', + description: + "S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.", + season: 6, + episode: 26, + icon: 'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b', + category: 'Intrattenimento/Fiction', + url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"events":[],"total":0}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/guide.dstv.com/guide.dstv.com.config.js b/sites/guide.dstv.com/guide.dstv.com.config.js index 44ca692ac..212cf8cc4 100644 --- a/sites/guide.dstv.com/guide.dstv.com.config.js +++ b/sites/guide.dstv.com/guide.dstv.com.config.js @@ -1,94 +1,94 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -// ERR: certificate has expired -module.exports = { - site: 'guide.dstv.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000, - interpretHeader: false - } - }, - url({ channel, date }) { - const [bouquetId] = channel.site_id.split('#') - - return `https://guide.dstv.com/api/gridview/page?bouquetId=${bouquetId}&genre=all&date=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser({ content, date, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } else if (start.hour() > 12) { - start = start.subtract(1, 'd') - date = date.subtract(1, 'd') - } - const stop = start.add(1, 'h') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - }, - async channels({ bouquet }) { - const data = await axios - .get( - `https://guide.dstv.com/api/channel/fetchChannelsByGenresInBouquet?bouquetId=${bouquet}&genre=all` - ) - .then(r => r.data) - .catch(console.log) - - const items = data.items - return items.map(item => { - return { - lang: 'en', - site_id: `${bouquet}#${item.channelTag}`, - name: item.channelName - } - }) - } -} - -function parseStart(item, date) { - const time = `${date.format('MM/DD/YYYY')} ${item.time}` - - return dayjs.utc(time, 'MM/DD/YYYY HH:mm') -} - -function parseItems(content, channel) { - const [, channelTag] = channel.site_id.split('#') - const data = JSON.parse(content) - const html = data[channelTag] - if (!html) return [] - const $ = cheerio.load(html) - - return $('li') - .map((i, el) => { - return { - time: $(el).find('.event-time').text().trim(), - title: $(el).find('.event-title').text().trim() - } - }) - .toArray() - .filter(i => i.time && i.title) -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +// ERR: certificate has expired +module.exports = { + site: 'guide.dstv.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000, + interpretHeader: false + } + }, + url({ channel, date }) { + const [bouquetId] = channel.site_id.split('#') + + return `https://guide.dstv.com/api/gridview/page?bouquetId=${bouquetId}&genre=all&date=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser({ content, date, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } else if (start.hour() > 12) { + start = start.subtract(1, 'd') + date = date.subtract(1, 'd') + } + const stop = start.add(1, 'h') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + }, + async channels({ bouquet }) { + const data = await axios + .get( + `https://guide.dstv.com/api/channel/fetchChannelsByGenresInBouquet?bouquetId=${bouquet}&genre=all` + ) + .then(r => r.data) + .catch(console.log) + + const items = data.items + return items.map(item => { + return { + lang: 'en', + site_id: `${bouquet}#${item.channelTag}`, + name: item.channelName + } + }) + } +} + +function parseStart(item, date) { + const time = `${date.format('MM/DD/YYYY')} ${item.time}` + + return dayjs.utc(time, 'MM/DD/YYYY HH:mm') +} + +function parseItems(content, channel) { + const [, channelTag] = channel.site_id.split('#') + const data = JSON.parse(content) + const html = data[channelTag] + if (!html) return [] + const $ = cheerio.load(html) + + return $('li') + .map((i, el) => { + return { + time: $(el).find('.event-time').text().trim(), + title: $(el).find('.event-title').text().trim() + } + }) + .toArray() + .filter(i => i.time && i.title) +} diff --git a/sites/guide.dstv.com/guide.dstv.com.test.js b/sites/guide.dstv.com/guide.dstv.com.test.js index 09b242e50..4e4f18f16 100644 --- a/sites/guide.dstv.com/guide.dstv.com.test.js +++ b/sites/guide.dstv.com/guide.dstv.com.test.js @@ -1,59 +1,59 @@ -// npm run grab -- --site=guide.dstv.com -// npm run channels:parse -- --config=./sites/guide.dstv.com/guide.dstv.com.config.js --output=./sites/guide.dstv.com/guide.dstv.com.channels.xml --set=bouquet:c35aaecd-5dd1-480b-ae24-357e600a0e4d - -const { parser, url } = require('./guide.dstv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'b0dc42b8-c651-4c3c-8713-a7fcd04744ee#M4H', - xmltv_id: 'MNetMovies4.za' -} - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://guide.dstv.com/api/gridview/page?bouquetId=b0dc42b8-c651-4c3c-8713-a7fcd04744ee&genre=all&date=2021-11-24' - ) -}) - -it('can parse response', () => { - const content = - "{\"M4H\": \"
  • 21:30

    Deadly Flight

  • 08:25

    I Still Believe

  • 15:50

    Despicable Me

  • 20:35

    The Foreigner

  • \"}" - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-23T21:30:00.000Z', - stop: '2021-11-24T08:25:00.000Z', - title: 'Deadly Flight' - }, - { - start: '2021-11-24T08:25:00.000Z', - stop: '2021-11-24T15:50:00.000Z', - title: 'I Still Believe' - }, - { - start: '2021-11-24T15:50:00.000Z', - stop: '2021-11-24T20:35:00.000Z', - title: 'Despicable Me' - }, - { - start: '2021-11-24T20:35:00.000Z', - stop: '2021-11-24T21:35:00.000Z', - title: 'The Foreigner' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{}' }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=guide.dstv.com +// npm run channels:parse -- --config=./sites/guide.dstv.com/guide.dstv.com.config.js --output=./sites/guide.dstv.com/guide.dstv.com.channels.xml --set=bouquet:c35aaecd-5dd1-480b-ae24-357e600a0e4d + +const { parser, url } = require('./guide.dstv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'b0dc42b8-c651-4c3c-8713-a7fcd04744ee#M4H', + xmltv_id: 'MNetMovies4.za' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://guide.dstv.com/api/gridview/page?bouquetId=b0dc42b8-c651-4c3c-8713-a7fcd04744ee&genre=all&date=2021-11-24' + ) +}) + +it('can parse response', () => { + const content = + "{\"M4H\": \"
  • 21:30

    Deadly Flight

  • 08:25

    I Still Believe

  • 15:50

    Despicable Me

  • 20:35

    The Foreigner

  • \"}" + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-23T21:30:00.000Z', + stop: '2021-11-24T08:25:00.000Z', + title: 'Deadly Flight' + }, + { + start: '2021-11-24T08:25:00.000Z', + stop: '2021-11-24T15:50:00.000Z', + title: 'I Still Believe' + }, + { + start: '2021-11-24T15:50:00.000Z', + stop: '2021-11-24T20:35:00.000Z', + title: 'Despicable Me' + }, + { + start: '2021-11-24T20:35:00.000Z', + stop: '2021-11-24T21:35:00.000Z', + title: 'The Foreigner' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/hd-plus.de/hd-plus.de.config.js b/sites/hd-plus.de/hd-plus.de.config.js index 9b960271e..dd1138788 100644 --- a/sites/hd-plus.de/hd-plus.de.config.js +++ b/sites/hd-plus.de/hd-plus.de.config.js @@ -1,57 +1,57 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'hd-plus.de', - days: 2, - url({ date, channel }) { - const today = dayjs().utc().startOf('d') - const day = date.diff(today, 'd') - - return `https://www.hd-plus.de/epg/channel/${channel.site_id}?d=${day}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ title: parseTitle($item), start, stop }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('td:nth-child(2)').text().split(' ').pop() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Berlin') -} - -function parseTitle($item) { - return $item('td:nth-child(1) > a').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('table > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'hd-plus.de', + days: 2, + url({ date, channel }) { + const today = dayjs().utc().startOf('d') + const day = date.diff(today, 'd') + + return `https://www.hd-plus.de/epg/channel/${channel.site_id}?d=${day}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ title: parseTitle($item), start, stop }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('td:nth-child(2)').text().split(' ').pop() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Berlin') +} + +function parseTitle($item) { + return $item('td:nth-child(1) > a').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('table > tbody > tr').toArray() +} diff --git a/sites/hd-plus.de/hd-plus.de.test.js b/sites/hd-plus.de/hd-plus.de.test.js index d8cb96d46..ab1419694 100644 --- a/sites/hd-plus.de/hd-plus.de.test.js +++ b/sites/hd-plus.de/hd-plus.de.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=hd-plus.de - -const { parser, url } = require('./hd-plus.de.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1-2-3-tv-hd', - xmltv_id: '123tv.de' -} -const content = - '
    1-2-3.tv HD

    1-2-3.tv HD

    Titel Ausstrahlungszeit
    Ihre Lieblingsuhren Do 25.11 00:00
    Ihre Lieblingsuhren Do 25.11 01:00
    Flash DealsDo 25.11 06:00
    ' - -it('can generate valid url', () => { - const today = dayjs.utc().startOf('d') - expect(url({ channel, date: today })).toBe('https://www.hd-plus.de/epg/channel/1-2-3-tv-hd?d=0') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-25T00:00:00.000Z', - title: 'Ihre Lieblingsuhren' - }, - { - start: '2021-11-25T00:00:00.000Z', - stop: '2021-11-25T05:00:00.000Z', - title: 'Ihre Lieblingsuhren' - }, - { - start: '2021-11-25T05:00:00.000Z', - stop: '2021-11-25T06:00:00.000Z', - title: 'Flash Deals' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=hd-plus.de + +const { parser, url } = require('./hd-plus.de.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1-2-3-tv-hd', + xmltv_id: '123tv.de' +} +const content = + '
    1-2-3.tv HD

    1-2-3.tv HD

    Titel Ausstrahlungszeit
    Ihre Lieblingsuhren Do 25.11 00:00
    Ihre Lieblingsuhren Do 25.11 01:00
    Flash DealsDo 25.11 06:00
    ' + +it('can generate valid url', () => { + const today = dayjs.utc().startOf('d') + expect(url({ channel, date: today })).toBe('https://www.hd-plus.de/epg/channel/1-2-3-tv-hd?d=0') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-25T00:00:00.000Z', + title: 'Ihre Lieblingsuhren' + }, + { + start: '2021-11-25T00:00:00.000Z', + stop: '2021-11-25T05:00:00.000Z', + title: 'Ihre Lieblingsuhren' + }, + { + start: '2021-11-25T05:00:00.000Z', + stop: '2021-11-25T06:00:00.000Z', + title: 'Flash Deals' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/horizon.tv/horizon.tv.config.js b/sites/horizon.tv/horizon.tv.config.js index 0444317cc..225021ca5 100644 --- a/sites/horizon.tv/horizon.tv.config.js +++ b/sites/horizon.tv/horizon.tv.config.js @@ -1,145 +1,145 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web' - -module.exports = { - site: 'horizon.tv', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const d = date.format('YYYYMMDD') - const promises = [ - axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) - ] - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data, channel)) - } - }) - }) - .catch(console.error) - for (let item of items) { - const detail = await loadProgramDetails(item) - programs.push({ - title: item.t, - description: parseDescription(detail), - category: parseCategory(detail), - season: parseSeason(detail), - episode: parseEpisode(detail), - actors: parseActors(detail), - directors: parseDirectors(detail), - date: parseYear(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}/channels`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'sk', - site_id: item.id.replace('lgi-obolite-sk-prod-master:5-', ''), - name: item.title - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.i) return {} - const url = `${API_ENDPOINT}/listings/${item.i}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseStart(item) { - return dayjs(item.s) -} - -function parseStop(item) { - return dayjs(item.e) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = typeof content === 'string' ? JSON.parse(content) : content - if (!data || !Array.isArray(data.entries)) return [] - const entity = data.entries.find(e => e.o === `lgi-obolite-sk-prod-master:${channel.site_id}`) - return entity ? entity.l : [] -} - -function parseDescription(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.longDescription || null -} - -function parseCategory(detail) { - if (!detail) return [] - if (!detail.program) return [] - if (!detail.program.categories) return [] - let categories = [] - detail.program.categories.forEach(category => { - categories.push(category.title) - }) - return categories -} - -function parseSeason(detail) { - if (!detail) return null - if (!detail.program) return null - if (!detail.program.seriesNumber) return null - if (String(detail.program.seriesNumber).length > 2) return null - return detail.program.seriesNumber -} - -function parseEpisode(detail) { - if (!detail) return null - if (!detail.program) return null - if (!detail.program.seriesEpisodeNumber) return null - if (String(detail.program.seriesEpisodeNumber).length > 3) return null - return detail.program.seriesEpisodeNumber -} - -function parseDirectors(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.directors || [] -} - -function parseActors(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.cast || [] -} - -function parseYear(detail) { - if (!detail) return null - if (!detail.program) return null - return detail.program.year || null -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web' + +module.exports = { + site: 'horizon.tv', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const d = date.format('YYYYMMDD') + const promises = [ + axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) + ] + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data, channel)) + } + }) + }) + .catch(console.error) + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.t, + description: parseDescription(detail), + category: parseCategory(detail), + season: parseSeason(detail), + episode: parseEpisode(detail), + actors: parseActors(detail), + directors: parseDirectors(detail), + date: parseYear(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}/channels`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'sk', + site_id: item.id.replace('lgi-obolite-sk-prod-master:5-', ''), + name: item.title + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.i) return {} + const url = `${API_ENDPOINT}/listings/${item.i}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseStart(item) { + return dayjs(item.s) +} + +function parseStop(item) { + return dayjs(item.e) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = typeof content === 'string' ? JSON.parse(content) : content + if (!data || !Array.isArray(data.entries)) return [] + const entity = data.entries.find(e => e.o === `lgi-obolite-sk-prod-master:${channel.site_id}`) + return entity ? entity.l : [] +} + +function parseDescription(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.longDescription || null +} + +function parseCategory(detail) { + if (!detail) return [] + if (!detail.program) return [] + if (!detail.program.categories) return [] + let categories = [] + detail.program.categories.forEach(category => { + categories.push(category.title) + }) + return categories +} + +function parseSeason(detail) { + if (!detail) return null + if (!detail.program) return null + if (!detail.program.seriesNumber) return null + if (String(detail.program.seriesNumber).length > 2) return null + return detail.program.seriesNumber +} + +function parseEpisode(detail) { + if (!detail) return null + if (!detail.program) return null + if (!detail.program.seriesEpisodeNumber) return null + if (String(detail.program.seriesEpisodeNumber).length > 3) return null + return detail.program.seriesEpisodeNumber +} + +function parseDirectors(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.directors || [] +} + +function parseActors(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.cast || [] +} + +function parseYear(detail) { + if (!detail) return null + if (!detail.program) return null + return detail.program.year || null +} diff --git a/sites/horizon.tv/horizon.tv.test.js b/sites/horizon.tv/horizon.tv.test.js index 4a5a67583..ccf6af4ad 100644 --- a/sites/horizon.tv/horizon.tv.test.js +++ b/sites/horizon.tv/horizon.tv.test.js @@ -1,266 +1,266 @@ -// npm run channels:parse -- --config=./sites/horizon.tv/horizon.tv.config.js --output=./sites/horizon.tv/horizon.tv.channels.xml -// npm run grab -- --site=horizon.tv - -const { parser, url } = require('./horizon.tv.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-02-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '10024', - xmltv_id: 'AMCCzechRepublic.cz' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1' - ) -}) - -it('can parse response', done => { - const content = - '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791343825,"title":"EPG","periods":4,"periodStartTime":1675724400000,"periodEndTime":1675746000000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","t":"Avengement","s":1675719300000,"e":1675724700000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - - axios.get.mockImplementation(url => { - if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791376097,"title":"EPG","periods":4,"periodStartTime":1675746000000,"periodEndTime":1675767600000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","t":"Zoom In","s":1675744500000,"e":1675746000000,"c":"lgi-obolite-sk-prod-master:genre-21","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675791024984,"title":"EPG","periods":4,"periodStartTime":1675767600000,"periodEndTime":1675789200000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","t":"Studentka","s":1675761000000,"e":1675767600000,"c":"lgi-obolite-sk-prod-master:genre-14","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675790973469,"title":"EPG","periods":4,"periodStartTime":1675789200000,"periodEndTime":1675810800000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","t":"Zilionáři","s":1675785900000,"e":1675791900000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","startTime":1675719300000,"endTime":1675724700000,"actualStartTime":1675719300000,"actualEndTime":1675724700000,"expirationDate":1676324100000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","scCridImi":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","program":{"id":"crid:~~2F~~2Fport.cs~~2F122941980","title":"Avengement","description":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","longDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"18","cast":["Scott Adkins","Craig Fairbrass","Thomas Turgoose","Nick Moran","Kierston Wareing","Leo Gregory","Mark Strange","Luke LaFontaine","Beau Fowler","Dan Styles","Christopher Sciueref","Matt Routledge","Jane Thorne","Louis Mandylor","Terence Maynard","Greg Burridge","Michael Higgs","Damian Gallagher","Daniel Adegboyega","John Ioannou","Sofie Golding-Spittle","Joe Egan","Darren Swain","Lee Charles","Dominic Kinnaird","Ross O\'Hennessy","Teresa Mahoney","Andrew Dunkelberger","Sam Hardy","Ivan Moy","Mark Sears","Phillip Ray Tommy"],"directors":["Jesse V. Johnson"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_3fa8387df870473fdacb1024635b52b2496b159c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_19e3a660e637cd39e31046c284a66b3a95d698e4.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","shortDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","mediaType":"FeatureFilm","year":"2019","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676247300000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","startTime":1675744500000,"endTime":1675746000000,"actualStartTime":1675744500000,"actualEndTime":1675746000000,"expirationDate":1676349300000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:e85129f9d1e211406a521df7a36f22237c22651b","scCridImi":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","program":{"id":"crid:~~2F~~2Fport.cs~~2F248281986","title":"Zoom In","description":"Film/Kino","longDescription":"Film/Kino","medium":"TV","categories":[{"id":"lgi-obolite-sk-prod-master:genre-21","title":"Hudba a umenie","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":[],"directors":[],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_cbed64b557e83227a2292604cbcae2d193877b1c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=180&h=260&mode=box"}],"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","shortDescription":"Film/Kino","mediaType":"Episode","year":"2010","seriesEpisodeNumber":"1302070535","seriesNumber":"1302080520","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675746000000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","startTime":1675761000000,"endTime":1675767600000,"actualStartTime":1675761000000,"actualEndTime":1675767600000,"expirationDate":1676365800000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","scCridImi":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","program":{"id":"crid:~~2F~~2Fport.cs~~2F1379541","title":"Studentka","description":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","longDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-4","title":"Komédia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":["Sophie Marceauová","Vincent Lindon","Elisabeth Vitali","Elena Pompei","Jean-Claude Leguay","Brigitte Chamarande","Christian Pereira","Gérard Dacier","Roberto Attias","Beppe Chierici","Nathalie Mann","Anne Macina","Janine Souchon","Virginie Demians","Hugues Leforestier","Jacqueline Noëlle","Marc-André Brunet","Isabelle Caubère","André Chazel","Med Salah Cheurfi","Guillaume Corea","Eric Denize","Gilles Gaston-Dreyfuss","Benoît Gourley","Marc Innocenti","Najim Laouriga","Laurent Ledermann","Philippe Maygal","Dominique Pifarely","Ysé Tran"],"directors":["Francis De Gueltz","Dominique Talmon","Claude Pinoteau"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_a8abceaa59bbb0aae8031dcdd5deba03aba8a100.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","shortDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","mediaType":"FeatureFilm","year":"1988","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675767600000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","startTime":1675785900000,"endTime":1675791900000,"actualStartTime":1675785900000,"actualEndTime":1675791900000,"expirationDate":1676390700000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","scCridImi":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","program":{"id":"crid:~~2F~~2Fport.cs~~2F71927954","title":"Zilionáři","description":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","longDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"15","cast":["Zach Galifianakis","Kristen Wiigová","Owen Wilson","Kate McKinnon","Leslie Jones","Jason Sudeikis","Ross Kimball","Devin Ratray","Mary Elizabeth Ellisová","Jon Daly","Ken Marino","Daniel Zacapa","Tom Werme","Njema Williams","Nils Cruz","Michael Fraguada","Christian Gonzalez","Candace Blanchard","Karsten Friske","Dallas Edwards","Barry Ratcliffe","Shelton Grant","Laura Palka","Reegus Flenory","Wynn Reichert","Jill Jane Clements","Joseph S. Wilson","Jee An","Rhoda Griffisová","Nicole Dupre Sobchack"],"directors":["Scott August","Richard L. Fox","Michelle Malley-Campos","Sebastian Mazzola","Steven Ritzi","Pete Waterman","Jared Hess"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_fd098116bac1429318aaf5fdae498ce76e258782.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_6f857ae9375b3bcceb6353a5b35775f52cd85302.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","shortDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","mediaType":"FeatureFilm","year":"2016","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676187900000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-02-06T21:35:00.000Z', - stop: '2023-02-06T23:05:00.000Z', - title: 'Avengement', - description: - 'Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.', - category: ['Drama', 'Akcia'], - directors: ['Jesse V. Johnson'], - actors: [ - 'Scott Adkins', - 'Craig Fairbrass', - 'Thomas Turgoose', - 'Nick Moran', - 'Kierston Wareing', - 'Leo Gregory', - 'Mark Strange', - 'Luke LaFontaine', - 'Beau Fowler', - 'Dan Styles', - 'Christopher Sciueref', - 'Matt Routledge', - 'Jane Thorne', - 'Louis Mandylor', - 'Terence Maynard', - 'Greg Burridge', - 'Michael Higgs', - 'Damian Gallagher', - 'Daniel Adegboyega', - 'John Ioannou', - 'Sofie Golding-Spittle', - 'Joe Egan', - 'Darren Swain', - 'Lee Charles', - 'Dominic Kinnaird', - "Ross O'Hennessy", - 'Teresa Mahoney', - 'Andrew Dunkelberger', - 'Sam Hardy', - 'Ivan Moy', - 'Mark Sears', - 'Phillip Ray Tommy' - ], - date: '2019' - }, - { - start: '2023-02-07T04:35:00.000Z', - stop: '2023-02-07T05:00:00.000Z', - title: 'Zoom In', - description: 'Film/Kino', - category: ['Hudba a umenie', 'Film'], - date: '2010' - }, - { - start: '2023-02-07T09:10:00.000Z', - stop: '2023-02-07T11:00:00.000Z', - title: 'Studentka', - description: - 'Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?', - category: ['Film', 'Komédia'], - actors: [ - 'Sophie Marceauová', - 'Vincent Lindon', - 'Elisabeth Vitali', - 'Elena Pompei', - 'Jean-Claude Leguay', - 'Brigitte Chamarande', - 'Christian Pereira', - 'Gérard Dacier', - 'Roberto Attias', - 'Beppe Chierici', - 'Nathalie Mann', - 'Anne Macina', - 'Janine Souchon', - 'Virginie Demians', - 'Hugues Leforestier', - 'Jacqueline Noëlle', - 'Marc-André Brunet', - 'Isabelle Caubère', - 'André Chazel', - 'Med Salah Cheurfi', - 'Guillaume Corea', - 'Eric Denize', - 'Gilles Gaston-Dreyfuss', - 'Benoît Gourley', - 'Marc Innocenti', - 'Najim Laouriga', - 'Laurent Ledermann', - 'Philippe Maygal', - 'Dominique Pifarely', - 'Ysé Tran' - ], - directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'], - date: '1988' - }, - { - start: '2023-02-07T16:05:00.000Z', - stop: '2023-02-07T17:45:00.000Z', - title: 'Zilionáři', - description: - 'David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...', - category: ['Drama', 'Akcia'], - actors: [ - 'Zach Galifianakis', - 'Kristen Wiigová', - 'Owen Wilson', - 'Kate McKinnon', - 'Leslie Jones', - 'Jason Sudeikis', - 'Ross Kimball', - 'Devin Ratray', - 'Mary Elizabeth Ellisová', - 'Jon Daly', - 'Ken Marino', - 'Daniel Zacapa', - 'Tom Werme', - 'Njema Williams', - 'Nils Cruz', - 'Michael Fraguada', - 'Christian Gonzalez', - 'Candace Blanchard', - 'Karsten Friske', - 'Dallas Edwards', - 'Barry Ratcliffe', - 'Shelton Grant', - 'Laura Palka', - 'Reegus Flenory', - 'Wynn Reichert', - 'Jill Jane Clements', - 'Joseph S. Wilson', - 'Jee An', - 'Rhoda Griffisová', - 'Nicole Dupre Sobchack' - ], - directors: [ - 'Scott August', - 'Richard L. Fox', - 'Michelle Malley-Campos', - 'Sebastian Mazzola', - 'Steven Ritzi', - 'Pete Waterman', - 'Jared Hess' - ], - date: '2016' - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', - channel, - date - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run channels:parse -- --config=./sites/horizon.tv/horizon.tv.config.js --output=./sites/horizon.tv/horizon.tv.channels.xml +// npm run grab -- --site=horizon.tv + +const { parser, url } = require('./horizon.tv.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-02-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '10024', + xmltv_id: 'AMCCzechRepublic.cz' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1' + ) +}) + +it('can parse response', done => { + const content = + '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791343825,"title":"EPG","periods":4,"periodStartTime":1675724400000,"periodEndTime":1675746000000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","t":"Avengement","s":1675719300000,"e":1675724700000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + + axios.get.mockImplementation(url => { + if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791376097,"title":"EPG","periods":4,"periodStartTime":1675746000000,"periodEndTime":1675767600000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","t":"Zoom In","s":1675744500000,"e":1675746000000,"c":"lgi-obolite-sk-prod-master:genre-21","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675791024984,"title":"EPG","periods":4,"periodStartTime":1675767600000,"periodEndTime":1675789200000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","t":"Studentka","s":1675761000000,"e":1675767600000,"c":"lgi-obolite-sk-prod-master:genre-14","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675790973469,"title":"EPG","periods":4,"periodStartTime":1675789200000,"periodEndTime":1675810800000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","t":"Zilionáři","s":1675785900000,"e":1675791900000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","startTime":1675719300000,"endTime":1675724700000,"actualStartTime":1675719300000,"actualEndTime":1675724700000,"expirationDate":1676324100000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","scCridImi":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","program":{"id":"crid:~~2F~~2Fport.cs~~2F122941980","title":"Avengement","description":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","longDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"18","cast":["Scott Adkins","Craig Fairbrass","Thomas Turgoose","Nick Moran","Kierston Wareing","Leo Gregory","Mark Strange","Luke LaFontaine","Beau Fowler","Dan Styles","Christopher Sciueref","Matt Routledge","Jane Thorne","Louis Mandylor","Terence Maynard","Greg Burridge","Michael Higgs","Damian Gallagher","Daniel Adegboyega","John Ioannou","Sofie Golding-Spittle","Joe Egan","Darren Swain","Lee Charles","Dominic Kinnaird","Ross O\'Hennessy","Teresa Mahoney","Andrew Dunkelberger","Sam Hardy","Ivan Moy","Mark Sears","Phillip Ray Tommy"],"directors":["Jesse V. Johnson"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_3fa8387df870473fdacb1024635b52b2496b159c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_19e3a660e637cd39e31046c284a66b3a95d698e4.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","shortDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","mediaType":"FeatureFilm","year":"2019","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676247300000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' + ) + }) + } else if ( + url === + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","startTime":1675744500000,"endTime":1675746000000,"actualStartTime":1675744500000,"actualEndTime":1675746000000,"expirationDate":1676349300000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:e85129f9d1e211406a521df7a36f22237c22651b","scCridImi":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","program":{"id":"crid:~~2F~~2Fport.cs~~2F248281986","title":"Zoom In","description":"Film/Kino","longDescription":"Film/Kino","medium":"TV","categories":[{"id":"lgi-obolite-sk-prod-master:genre-21","title":"Hudba a umenie","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":[],"directors":[],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_cbed64b557e83227a2292604cbcae2d193877b1c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=180&h=260&mode=box"}],"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","shortDescription":"Film/Kino","mediaType":"Episode","year":"2010","seriesEpisodeNumber":"1302070535","seriesNumber":"1302080520","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675746000000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' + ) + }) + } else if ( + url === + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","startTime":1675761000000,"endTime":1675767600000,"actualStartTime":1675761000000,"actualEndTime":1675767600000,"expirationDate":1676365800000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","scCridImi":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","program":{"id":"crid:~~2F~~2Fport.cs~~2F1379541","title":"Studentka","description":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","longDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-4","title":"Komédia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":["Sophie Marceauová","Vincent Lindon","Elisabeth Vitali","Elena Pompei","Jean-Claude Leguay","Brigitte Chamarande","Christian Pereira","Gérard Dacier","Roberto Attias","Beppe Chierici","Nathalie Mann","Anne Macina","Janine Souchon","Virginie Demians","Hugues Leforestier","Jacqueline Noëlle","Marc-André Brunet","Isabelle Caubère","André Chazel","Med Salah Cheurfi","Guillaume Corea","Eric Denize","Gilles Gaston-Dreyfuss","Benoît Gourley","Marc Innocenti","Najim Laouriga","Laurent Ledermann","Philippe Maygal","Dominique Pifarely","Ysé Tran"],"directors":["Francis De Gueltz","Dominique Talmon","Claude Pinoteau"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_a8abceaa59bbb0aae8031dcdd5deba03aba8a100.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","shortDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","mediaType":"FeatureFilm","year":"1988","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675767600000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' + ) + }) + } else if ( + url === + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","startTime":1675785900000,"endTime":1675791900000,"actualStartTime":1675785900000,"actualEndTime":1675791900000,"expirationDate":1676390700000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","scCridImi":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","program":{"id":"crid:~~2F~~2Fport.cs~~2F71927954","title":"Zilionáři","description":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","longDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"15","cast":["Zach Galifianakis","Kristen Wiigová","Owen Wilson","Kate McKinnon","Leslie Jones","Jason Sudeikis","Ross Kimball","Devin Ratray","Mary Elizabeth Ellisová","Jon Daly","Ken Marino","Daniel Zacapa","Tom Werme","Njema Williams","Nils Cruz","Michael Fraguada","Christian Gonzalez","Candace Blanchard","Karsten Friske","Dallas Edwards","Barry Ratcliffe","Shelton Grant","Laura Palka","Reegus Flenory","Wynn Reichert","Jill Jane Clements","Joseph S. Wilson","Jee An","Rhoda Griffisová","Nicole Dupre Sobchack"],"directors":["Scott August","Richard L. Fox","Michelle Malley-Campos","Sebastian Mazzola","Steven Ritzi","Pete Waterman","Jared Hess"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_fd098116bac1429318aaf5fdae498ce76e258782.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_6f857ae9375b3bcceb6353a5b35775f52cd85302.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","shortDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","mediaType":"FeatureFilm","year":"2016","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676187900000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' + ) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-02-06T21:35:00.000Z', + stop: '2023-02-06T23:05:00.000Z', + title: 'Avengement', + description: + 'Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.', + category: ['Drama', 'Akcia'], + directors: ['Jesse V. Johnson'], + actors: [ + 'Scott Adkins', + 'Craig Fairbrass', + 'Thomas Turgoose', + 'Nick Moran', + 'Kierston Wareing', + 'Leo Gregory', + 'Mark Strange', + 'Luke LaFontaine', + 'Beau Fowler', + 'Dan Styles', + 'Christopher Sciueref', + 'Matt Routledge', + 'Jane Thorne', + 'Louis Mandylor', + 'Terence Maynard', + 'Greg Burridge', + 'Michael Higgs', + 'Damian Gallagher', + 'Daniel Adegboyega', + 'John Ioannou', + 'Sofie Golding-Spittle', + 'Joe Egan', + 'Darren Swain', + 'Lee Charles', + 'Dominic Kinnaird', + "Ross O'Hennessy", + 'Teresa Mahoney', + 'Andrew Dunkelberger', + 'Sam Hardy', + 'Ivan Moy', + 'Mark Sears', + 'Phillip Ray Tommy' + ], + date: '2019' + }, + { + start: '2023-02-07T04:35:00.000Z', + stop: '2023-02-07T05:00:00.000Z', + title: 'Zoom In', + description: 'Film/Kino', + category: ['Hudba a umenie', 'Film'], + date: '2010' + }, + { + start: '2023-02-07T09:10:00.000Z', + stop: '2023-02-07T11:00:00.000Z', + title: 'Studentka', + description: + 'Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?', + category: ['Film', 'Komédia'], + actors: [ + 'Sophie Marceauová', + 'Vincent Lindon', + 'Elisabeth Vitali', + 'Elena Pompei', + 'Jean-Claude Leguay', + 'Brigitte Chamarande', + 'Christian Pereira', + 'Gérard Dacier', + 'Roberto Attias', + 'Beppe Chierici', + 'Nathalie Mann', + 'Anne Macina', + 'Janine Souchon', + 'Virginie Demians', + 'Hugues Leforestier', + 'Jacqueline Noëlle', + 'Marc-André Brunet', + 'Isabelle Caubère', + 'André Chazel', + 'Med Salah Cheurfi', + 'Guillaume Corea', + 'Eric Denize', + 'Gilles Gaston-Dreyfuss', + 'Benoît Gourley', + 'Marc Innocenti', + 'Najim Laouriga', + 'Laurent Ledermann', + 'Philippe Maygal', + 'Dominique Pifarely', + 'Ysé Tran' + ], + directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'], + date: '1988' + }, + { + start: '2023-02-07T16:05:00.000Z', + stop: '2023-02-07T17:45:00.000Z', + title: 'Zilionáři', + description: + 'David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...', + category: ['Drama', 'Akcia'], + actors: [ + 'Zach Galifianakis', + 'Kristen Wiigová', + 'Owen Wilson', + 'Kate McKinnon', + 'Leslie Jones', + 'Jason Sudeikis', + 'Ross Kimball', + 'Devin Ratray', + 'Mary Elizabeth Ellisová', + 'Jon Daly', + 'Ken Marino', + 'Daniel Zacapa', + 'Tom Werme', + 'Njema Williams', + 'Nils Cruz', + 'Michael Fraguada', + 'Christian Gonzalez', + 'Candace Blanchard', + 'Karsten Friske', + 'Dallas Edwards', + 'Barry Ratcliffe', + 'Shelton Grant', + 'Laura Palka', + 'Reegus Flenory', + 'Wynn Reichert', + 'Jill Jane Clements', + 'Joseph S. Wilson', + 'Jee An', + 'Rhoda Griffisová', + 'Nicole Dupre Sobchack' + ], + directors: [ + 'Scott August', + 'Richard L. Fox', + 'Michelle Malley-Campos', + 'Sebastian Mazzola', + 'Steven Ritzi', + 'Pete Waterman', + 'Jared Hess' + ], + date: '2016' + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', + channel, + date + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/i.mjh.nz/i.mjh.nz.config.js b/sites/i.mjh.nz/i.mjh.nz.config.js index 0f81dbf6a..1c17a6de4 100644 --- a/sites/i.mjh.nz/i.mjh.nz.config.js +++ b/sites/i.mjh.nz/i.mjh.nz.config.js @@ -1,107 +1,107 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const parser = require('epg-parser') -const isBetween = require('dayjs/plugin/isBetween') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(isBetween) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master' - -module.exports = { - site: 'i.mjh.nz', - days: 2, - request: { - cache: { - ttl: 3 * 60 * 60 * 1000 // 3h - }, - maxContentLength: 100 * 1024 * 1024 // 100Mb - }, - url: function ({ channel }) { - const [path] = channel.site_id.split('#') - - return `${API_ENDPOINT}/${path}.xml` - }, - parser: function ({ content, channel, date }) { - const items = parseItems(content, channel, date) - - let programs = items.map(item => { - return { - ...item, - title: getTitle(item), - description: getDescription(item), - categories: getCategories(item) - } - }) - - programs = mergeMovieParts(programs) - - return programs - }, - async channels({ path, lang = 'en' }) { - let xml = await axios - .get(`${API_ENDPOINT}/${path}.xml`) - .then(r => r.data) - .catch(console.log) - let data = parser.parse(xml) - - return data.channels.map(channel => { - return { - lang, - site_id: `${path}#${channel.id}`, - name: channel.name[0].value - } - }) - } -} - -function mergeMovieParts(programs) { - let output = [] - - programs.forEach(prog => { - let prev = output[output.length - 1] - let found = - prev && - prog.categories.includes('Movie') && - prev.title === prog.title && - prev.description === prog.description - - if (found) { - prev.stop = prog.stop - } else { - output.push(prog) - } - }) - - return output -} - -function getTitle(item) { - return item.title.length ? item.title[0].value : null -} - -function getDescription(item) { - return item.desc.length ? item.desc[0].value : null -} - -function getCategories(item) { - return item.category.map(c => c.value) -} - -function parseItems(content, channel, date) { - try { - const curr_day = date - const next_day = date.add(1, 'd') - const [, site_id] = channel.site_id.split('#') - const data = parser.parse(content) - if (!data || !Array.isArray(data.programs)) return [] - - return data.programs.filter( - p => - p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day) - ) - } catch (error) { - return [] - } -} +const dayjs = require('dayjs') +const axios = require('axios') +const parser = require('epg-parser') +const isBetween = require('dayjs/plugin/isBetween') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(isBetween) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master' + +module.exports = { + site: 'i.mjh.nz', + days: 2, + request: { + cache: { + ttl: 3 * 60 * 60 * 1000 // 3h + }, + maxContentLength: 100 * 1024 * 1024 // 100Mb + }, + url: function ({ channel }) { + const [path] = channel.site_id.split('#') + + return `${API_ENDPOINT}/${path}.xml` + }, + parser: function ({ content, channel, date }) { + const items = parseItems(content, channel, date) + + let programs = items.map(item => { + return { + ...item, + title: getTitle(item), + description: getDescription(item), + categories: getCategories(item) + } + }) + + programs = mergeMovieParts(programs) + + return programs + }, + async channels({ path, lang = 'en' }) { + let xml = await axios + .get(`${API_ENDPOINT}/${path}.xml`) + .then(r => r.data) + .catch(console.log) + let data = parser.parse(xml) + + return data.channels.map(channel => { + return { + lang, + site_id: `${path}#${channel.id}`, + name: channel.name[0].value + } + }) + } +} + +function mergeMovieParts(programs) { + let output = [] + + programs.forEach(prog => { + let prev = output[output.length - 1] + let found = + prev && + prog.categories.includes('Movie') && + prev.title === prog.title && + prev.description === prog.description + + if (found) { + prev.stop = prog.stop + } else { + output.push(prog) + } + }) + + return output +} + +function getTitle(item) { + return item.title.length ? item.title[0].value : null +} + +function getDescription(item) { + return item.desc.length ? item.desc[0].value : null +} + +function getCategories(item) { + return item.category.map(c => c.value) +} + +function parseItems(content, channel, date) { + try { + const curr_day = date + const next_day = date.add(1, 'd') + const [, site_id] = channel.site_id.split('#') + const data = parser.parse(content) + if (!data || !Array.isArray(data.programs)) return [] + + return data.programs.filter( + p => + p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day) + ) + } catch (error) { + return [] + } +} diff --git a/sites/i.mjh.nz/i.mjh.nz.test.js b/sites/i.mjh.nz/i.mjh.nz.test.js index c5ba8bc85..8f74c2d72 100644 --- a/sites/i.mjh.nz/i.mjh.nz.test.js +++ b/sites/i.mjh.nz/i.mjh.nz.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/i.mjh.nz/i.mjh.nz.config.js --output=./sites/i.mjh.nz/i.mjh.nz_pluto.channels.xml --set=path:PlutoTV/all -// npm run grab -- --site=i.mjh.nz - -const { parser, url } = require('./i.mjh.nz.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-23', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Plex/all#5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', - xmltv_id: 'DarkMatterTV.us', - lang: 'en' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe( - 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master/Plex/all.xml' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - const results = parser({ content, channel, date }) - - expect(results.length).toBe(11) - expect(results[0]).toMatchObject({ - start: '2023-06-23T07:14:32.000Z', - stop: '2023-06-23T09:09:36.000Z', - title: 'Killers Within', - date: ['20180101'], - description: - 'With her son being held captive by a criminal gang, police officer Amanda Doyle, together with her ex-husband and three unlikely allies, takes part in a desperate plot to hold a wealthy banker and his family to ransom. But this is no ordinary family.', - icon: ['https://provider-static.plex.tv/epg/images/thumbnails/darkmatter-tv-fallback.jpg'], - categories: ['Movie'] - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '404: Not Found', - channel, - date - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/i.mjh.nz/i.mjh.nz.config.js --output=./sites/i.mjh.nz/i.mjh.nz_pluto.channels.xml --set=path:PlutoTV/all +// npm run grab -- --site=i.mjh.nz + +const { parser, url } = require('./i.mjh.nz.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-23', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Plex/all#5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', + xmltv_id: 'DarkMatterTV.us', + lang: 'en' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master/Plex/all.xml' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + const results = parser({ content, channel, date }) + + expect(results.length).toBe(11) + expect(results[0]).toMatchObject({ + start: '2023-06-23T07:14:32.000Z', + stop: '2023-06-23T09:09:36.000Z', + title: 'Killers Within', + date: ['20180101'], + description: + 'With her son being held captive by a criminal gang, police officer Amanda Doyle, together with her ex-husband and three unlikely allies, takes part in a desperate plot to hold a wealthy banker and his family to ransom. But this is no ordinary family.', + icon: ['https://provider-static.plex.tv/epg/images/thumbnails/darkmatter-tv-fallback.jpg'], + categories: ['Movie'] + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '404: Not Found', + channel, + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/i24news.tv/i24news.tv.config.js b/sites/i24news.tv/i24news.tv.config.js index 3c590bc5b..b67f1f5d6 100644 --- a/sites/i24news.tv/i24news.tv.config.js +++ b/sites/i24news.tv/i24news.tv.config.js @@ -1,67 +1,67 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'i24news.tv', - days: 2, - url: function ({ channel }) { - const [lang] = channel.site_id.split('#') - - return `https://api.i24news.tv/v2/${lang}/schedules/world` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - if (!item.show) return - programs.push({ - title: item.show.title, - description: item.show.body, - icon: parseIcon(item), - start: parseStart(item, date), - stop: parseStop(item, date) - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.show.image ? item.show.image.href : null -} - -function parseStart(item, date) { - if (!item.startHour) return null - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.startHour}`, - 'YYYY-MM-DD HH:mm', - 'Asia/Jerusalem' - ) -} - -function parseStop(item, date) { - if (!item.endHour) return null - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.endHour}`, - 'YYYY-MM-DD HH:mm', - 'Asia/Jerusalem' - ) -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - let day = date.day() - 1 - day = day < 0 ? 6 : day - - return data.filter(item => item.day === day) -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'i24news.tv', + days: 2, + url: function ({ channel }) { + const [lang] = channel.site_id.split('#') + + return `https://api.i24news.tv/v2/${lang}/schedules/world` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + if (!item.show) return + programs.push({ + title: item.show.title, + description: item.show.body, + icon: parseIcon(item), + start: parseStart(item, date), + stop: parseStop(item, date) + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.show.image ? item.show.image.href : null +} + +function parseStart(item, date) { + if (!item.startHour) return null + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.startHour}`, + 'YYYY-MM-DD HH:mm', + 'Asia/Jerusalem' + ) +} + +function parseStop(item, date) { + if (!item.endHour) return null + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.endHour}`, + 'YYYY-MM-DD HH:mm', + 'Asia/Jerusalem' + ) +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + let day = date.day() - 1 + day = day < 0 ? 6 : day + + return data.filter(item => item.day === day) +} diff --git a/sites/i24news.tv/i24news.tv.test.js b/sites/i24news.tv/i24news.tv.test.js index ab96ad072..f231d4e50 100644 --- a/sites/i24news.tv/i24news.tv.test.js +++ b/sites/i24news.tv/i24news.tv.test.js @@ -1,46 +1,46 @@ -// npm run grab -- --site=i24news.tv - -const { parser, url } = require('./i24news.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ar#', - xmltv_id: 'I24NewsArabic.il' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules/world') -}) - -it('can parse response', () => { - const content = - '[{"id":348995,"startHour":"22:30","endHour":"23:00","day":5,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}},{"id":349023,"startHour":"15:00","endHour":"15:28","day":6,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}}]' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-06T13:00:00.000Z', - stop: '2022-03-06T13:28:00.000Z', - title: 'تغطية خاصة', - description: 'Special Edition', - icon: 'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=i24news.tv + +const { parser, url } = require('./i24news.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ar#', + xmltv_id: 'I24NewsArabic.il' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules/world') +}) + +it('can parse response', () => { + const content = + '[{"id":348995,"startHour":"22:30","endHour":"23:00","day":5,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}},{"id":349023,"startHour":"15:00","endHour":"15:28","day":6,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}}]' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-06T13:00:00.000Z', + stop: '2022-03-06T13:28:00.000Z', + title: 'تغطية خاصة', + description: 'Special Edition', + icon: 'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/indihometv.com/indihometv.com.config.js b/sites/indihometv.com/indihometv.com.config.js index 618e909d0..57712c2c1 100644 --- a/sites/indihometv.com/indihometv.com.config.js +++ b/sites/indihometv.com/indihometv.com.config.js @@ -1,68 +1,68 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'indihometv.com', - days: 2, - url({ channel }) { - return `https://www.indihometv.com/tvod/${channel.site_id}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev && start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('p').text() - const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${start}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') -} - -function parseStop($item, date) { - const timeString = $item('p').text() - const [, stop] = timeString.match(/- (\d{2}:\d{2})/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${stop}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') -} - -function parseTitle($item) { - return $item('b').text() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - return $(`#pills-${date.format('YYYY-MM-DD')} .schedule-item`).toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'indihometv.com', + days: 2, + url({ channel }) { + return `https://www.indihometv.com/tvod/${channel.site_id}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev && start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('p').text() + const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${start}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') +} + +function parseStop($item, date) { + const timeString = $item('p').text() + const [, stop] = timeString.match(/- (\d{2}:\d{2})/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${stop}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') +} + +function parseTitle($item) { + return $item('b').text() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + return $(`#pills-${date.format('YYYY-MM-DD')} .schedule-item`).toArray() +} diff --git a/sites/indihometv.com/indihometv.com.test.js b/sites/indihometv.com/indihometv.com.test.js index 7d015d369..5f5c5ca03 100644 --- a/sites/indihometv.com/indihometv.com.test.js +++ b/sites/indihometv.com/indihometv.com.test.js @@ -1,58 +1,58 @@ -// npm run grab -- --site=indihometv.com - -const { parser, url } = require('./indihometv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'metrotv', - xmltv_id: 'MetroTV.id' -} -const content = - '
    ' - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.indihometv.com/tvod/metrotv') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Headline News', - start: '2022-08-08T00:00:00.000Z', - stop: '2022-08-08T00:05:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:05:00.000Z', - stop: '2022-08-08T00:30:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:30:00.000Z', - stop: '2022-08-08T00:45:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:45:00.000Z', - stop: '2022-08-08T01:00:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=indihometv.com + +const { parser, url } = require('./indihometv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'metrotv', + xmltv_id: 'MetroTV.id' +} +const content = + '
    ' + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.indihometv.com/tvod/metrotv') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Headline News', + start: '2022-08-08T00:00:00.000Z', + stop: '2022-08-08T00:05:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:05:00.000Z', + stop: '2022-08-08T00:30:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:30:00.000Z', + stop: '2022-08-08T00:45:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:45:00.000Z', + stop: '2022-08-08T01:00:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ionplustv.com/ionplustv.com.config.js b/sites/ionplustv.com/ionplustv.com.config.js index 6dab5fe15..eb370e9eb 100644 --- a/sites/ionplustv.com/ionplustv.com.config.js +++ b/sites/ionplustv.com/ionplustv.com.config.js @@ -1,107 +1,107 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'ionplustv.com', - days: 2, - url({ date }) { - return `https://ionplustv.com/schedule/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const duration = parseDuration($item) - let stop = start.add(duration, 'm') - - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - rating: parseRating($item), - start, - stop - }) - } - - return programs - } -} - -function parseDescription($item) { - return $item('.panel-body > div > div > div > p:nth-child(2)').text().trim() -} - -function parseIcon($item) { - return $item('.video-thumbnail img').attr('src') -} - -function parseTitle($item) { - return $item('.show-title').text().trim() -} - -function parseSubTitle($item) { - return $item('.panel-title > div > div > div > div:nth-child(2) > p') - .text() - .trim() - .replace(/\s\s+/g, ' ') -} - -function parseRating($item) { - const [, rating] = $item('.tv-rating') - .text() - .match(/([^(]+)/) || [null, null] - - return rating - ? { - system: 'MPA', - value: rating.trim() - } - : null -} - -function parseStart($item, date) { - let time = $item('.panel-title h2').clone().children().remove().end().text().trim() - time = time.includes(':') ? time : time + ':00' - const meridiem = $item('.panel-title h2 > .meridiem').text().trim() - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${time} ${meridiem}`, - 'YYYY-MM-DD H:mm A', - 'America/New_York' - ) -} - -function parseDuration($item) { - const [, duration] = $item('.tv-rating') - .text() - .trim() - .match(/\((\d+)/) || [null, null] - - return parseInt(duration) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('#accordion > div').toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'ionplustv.com', + days: 2, + url({ date }) { + return `https://ionplustv.com/schedule/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const duration = parseDuration($item) + let stop = start.add(duration, 'm') + + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + rating: parseRating($item), + start, + stop + }) + } + + return programs + } +} + +function parseDescription($item) { + return $item('.panel-body > div > div > div > p:nth-child(2)').text().trim() +} + +function parseIcon($item) { + return $item('.video-thumbnail img').attr('src') +} + +function parseTitle($item) { + return $item('.show-title').text().trim() +} + +function parseSubTitle($item) { + return $item('.panel-title > div > div > div > div:nth-child(2) > p') + .text() + .trim() + .replace(/\s\s+/g, ' ') +} + +function parseRating($item) { + const [, rating] = $item('.tv-rating') + .text() + .match(/([^(]+)/) || [null, null] + + return rating + ? { + system: 'MPA', + value: rating.trim() + } + : null +} + +function parseStart($item, date) { + let time = $item('.panel-title h2').clone().children().remove().end().text().trim() + time = time.includes(':') ? time : time + ':00' + const meridiem = $item('.panel-title h2 > .meridiem').text().trim() + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${time} ${meridiem}`, + 'YYYY-MM-DD H:mm A', + 'America/New_York' + ) +} + +function parseDuration($item) { + const [, duration] = $item('.tv-rating') + .text() + .trim() + .match(/\((\d+)/) || [null, null] + + return parseInt(duration) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('#accordion > div').toArray() +} diff --git a/sites/ionplustv.com/ionplustv.com.test.js b/sites/ionplustv.com/ionplustv.com.test.js index 076ca9f68..460bb72f0 100644 --- a/sites/ionplustv.com/ionplustv.com.test.js +++ b/sites/ionplustv.com/ionplustv.com.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=ionplustv.com - -const { parser, url } = require('./ionplustv.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://ionplustv.com/schedule/2022-11-08') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-08T10:00:00.000Z', - stop: '2022-11-08T11:00:00.000Z', - title: 'All For Nothing?', - sub_title: '226 : Randy & Sarita Vs. Jean-marcel & Melodie', - icon: 'https://ionplustv.com/static/programs/shows/all-for-nothing/show-banner-all-for-nothing-5ab162f2d8ee6-897aca6d7d9a7d4e2026ca3b592d8b2a047238fa.png', - rating: { - system: 'MPA', - value: 'TV-PG+L' - }, - description: - "Randy and Sarita want to take their relationship to the next level and move-in together. Blending their families will require space for seven so they must sell Randy's dated bungalow for top dollar. Paul and Penny have differing opinions on the best plan for this house, but they do agree that all the wallpaper boarders must go! Having struggled to get the demolition started, Randy and Sarita turn up the reno pace in the second week which includes gambling on a poker night fundraiser. In preparation for retirement, Jean-Marcel and Melodie are ready to downsize. Having been out of the real estate market for ages, they have no idea how to ˜wow' the buyers of today. Armed with Paul and Penny's job list to bring their house into the now, they make major progress on day one. Flu, leaks, and a free shower insert that won't fit into their bathroom slow down their pace giving the competition a chance to overtake their early lead." - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), - date - }) - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=ionplustv.com + +const { parser, url } = require('./ionplustv.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://ionplustv.com/schedule/2022-11-08') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-08T10:00:00.000Z', + stop: '2022-11-08T11:00:00.000Z', + title: 'All For Nothing?', + sub_title: '226 : Randy & Sarita Vs. Jean-marcel & Melodie', + icon: 'https://ionplustv.com/static/programs/shows/all-for-nothing/show-banner-all-for-nothing-5ab162f2d8ee6-897aca6d7d9a7d4e2026ca3b592d8b2a047238fa.png', + rating: { + system: 'MPA', + value: 'TV-PG+L' + }, + description: + "Randy and Sarita want to take their relationship to the next level and move-in together. Blending their families will require space for seven so they must sell Randy's dated bungalow for top dollar. Paul and Penny have differing opinions on the best plan for this house, but they do agree that all the wallpaper boarders must go! Having struggled to get the demolition started, Randy and Sarita turn up the reno pace in the second week which includes gambling on a poker night fundraiser. In preparation for retirement, Jean-Marcel and Melodie are ready to downsize. Having been out of the real estate market for ages, they have no idea how to ˜wow' the buyers of today. Armed with Paul and Penny's job list to bring their house into the now, they make major progress on day one. Flu, leaks, and a free shower insert that won't fit into their bathroom slow down their pace giving the competition a chance to overtake their early lead." + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), + date + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/ipko.com/ipko.com.config.js b/sites/ipko.com/ipko.com.config.js index b2f2dfd33..9d30ff084 100644 --- a/sites/ipko.com/ipko.com.config.js +++ b/sites/ipko.com/ipko.com.config.js @@ -1,46 +1,46 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'ipko.com', - days: 2, - url: function ({ date }) { - return `https://www.ipko.com/epg/admin/programs.php?date=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.duration / 3, 'm') - - programs.push({ - title: item.program_name, - description: item.description, - category: item.category, - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.date}` - - return dayjs.utc(time, 'YYYY-MM-DD HH:mm:ss') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - const arr = Object.values(data.element) - const items = arr.find(el => { - return el[0] && el[0].channel_id == channel.site_id - }) - - return Array.isArray(items) ? items : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'ipko.com', + days: 2, + url: function ({ date }) { + return `https://www.ipko.com/epg/admin/programs.php?date=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.duration / 3, 'm') + + programs.push({ + title: item.program_name, + description: item.description, + category: item.category, + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.date}` + + return dayjs.utc(time, 'YYYY-MM-DD HH:mm:ss') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + const arr = Object.values(data.element) + const items = arr.find(el => { + return el[0] && el[0].channel_id == channel.site_id + }) + + return Array.isArray(items) ? items : [] +} diff --git a/sites/ipko.com/ipko.com.test.js b/sites/ipko.com/ipko.com.test.js index dc0517a85..298175f5e 100644 --- a/sites/ipko.com/ipko.com.test.js +++ b/sites/ipko.com/ipko.com.test.js @@ -1,40 +1,40 @@ -// npm run grab -- --site=ipko.com - -const { parser, url } = require('./ipko.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '406', - xmltv_id: 'RTK1.xk' -} -const content = - '{"element":{"1":[{"id":6367,"channel_id":406,"program_name":"Beautiful People 13","name_short":"","description":"Lin largohet nga Nju Meksiko për t\'u vendosur në Nju Jork e për t\'ia nisur nga fillimi: një punë të re, shtëpi të re dhe njohje të reja. Bashkë me të janë vajzat e saj, Sofia, një 16 vjeçare që shkëlqen në shkollë, dhe Kareni, 20 vjeçare, që do të bë","category":"Sezoni I","duration":150,"day":"Sun","left_distanc":165,"date":"00:55:00"}]}}' - -it('can generate valid url', () => { - const result = url({ date }) - expect(result).toBe('https://www.ipko.com/epg/admin/programs.php?date=2021-10-24') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: 'Sun, 24 Oct 2021 00:55:00 GMT', - stop: 'Sun, 24 Oct 2021 01:45:00 GMT', - title: 'Beautiful People 13', - description: - "Lin largohet nga Nju Meksiko për t'u vendosur në Nju Jork e për t'ia nisur nga fillimi: një punë të re, shtëpi të re dhe njohje të reja. Bashkë me të janë vajzat e saj, Sofia, një 16 vjeçare që shkëlqen në shkollë, dhe Kareni, 20 vjeçare, që do të bë", - category: 'Sezoni I' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{"element":{"1":[{"no":"no"}]}}' }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=ipko.com + +const { parser, url } = require('./ipko.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '406', + xmltv_id: 'RTK1.xk' +} +const content = + '{"element":{"1":[{"id":6367,"channel_id":406,"program_name":"Beautiful People 13","name_short":"","description":"Lin largohet nga Nju Meksiko për t\'u vendosur në Nju Jork e për t\'ia nisur nga fillimi: një punë të re, shtëpi të re dhe njohje të reja. Bashkë me të janë vajzat e saj, Sofia, një 16 vjeçare që shkëlqen në shkollë, dhe Kareni, 20 vjeçare, që do të bë","category":"Sezoni I","duration":150,"day":"Sun","left_distanc":165,"date":"00:55:00"}]}}' + +it('can generate valid url', () => { + const result = url({ date }) + expect(result).toBe('https://www.ipko.com/epg/admin/programs.php?date=2021-10-24') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: 'Sun, 24 Oct 2021 00:55:00 GMT', + stop: 'Sun, 24 Oct 2021 01:45:00 GMT', + title: 'Beautiful People 13', + description: + "Lin largohet nga Nju Meksiko për t'u vendosur në Nju Jork e për t'ia nisur nga fillimi: një punë të re, shtëpi të re dhe njohje të reja. Bashkë me të janë vajzat e saj, Sofia, një 16 vjeçare që shkëlqen në shkollë, dhe Kareni, 20 vjeçare, që do të bë", + category: 'Sezoni I' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{"element":{"1":[{"no":"no"}]}}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/kan.org.il/kan.org.il.config.js b/sites/kan.org.il/kan.org.il.config.js index 987b4aa3a..0170d5509 100644 --- a/sites/kan.org.il/kan.org.il.config.js +++ b/sites/kan.org.il/kan.org.il.config.js @@ -1,52 +1,52 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'kan.org.il', - days: 2, - url: function ({ channel, date }) { - return `https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=${ - channel.site_id - }&day=${date.format('DD/MM/YYYY')}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.live_desc, - icon: item.picture_code, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - if (!item.start_time) return null - - return dayjs.tz(item.start_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') -} - -function parseStop(item) { - if (!item.end_time) return null - - return dayjs.tz(item.end_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - - return data -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'kan.org.il', + days: 2, + url: function ({ channel, date }) { + return `https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=${ + channel.site_id + }&day=${date.format('DD/MM/YYYY')}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.live_desc, + icon: item.picture_code, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + if (!item.start_time) return null + + return dayjs.tz(item.start_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') +} + +function parseStop(item) { + if (!item.end_time) return null + + return dayjs.tz(item.end_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + + return data +} diff --git a/sites/kan.org.il/kan.org.il.test.js b/sites/kan.org.il/kan.org.il.test.js index c2aa97101..616265b5c 100644 --- a/sites/kan.org.il/kan.org.il.test.js +++ b/sites/kan.org.il/kan.org.il.test.js @@ -1,48 +1,48 @@ -// npm run grab -- --site=kan.org.il - -const { parser, url } = require('./kan.org.il.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '19', - xmltv_id: 'KANEducational.il' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=19&day=06/03/2022' - ) -}) - -it('can parse response', () => { - const content = - '[{"title":"ארץ מולדת - בין תורכיה לבריטניה","start_time":"2022-03-06T00:05:37","end_time":"2022-03-06T00:27:12","id":"2598","age_category_desc":"0","epg_name":"ארץ מולדת","title1":"ארץ מולדת - בין תורכיה לבריטניה","chapter_number":"9","live_desc":"קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות\'מאני לבין תקוותיו מהבריטים הכובשים.","Station_Radio":"0","Station_Id":"20","stationUrlScheme":"kan11://plugin/?type=player&plugin_identifier=kan_player&ds=general-provider%3A%2F%2FfetchData%3Ftype%3DFEED_JSON%26url%3DaHR0cHM6Ly93d3cua2FuLm9yZy5pbC9hcHBLYW4vbGl2ZVN0YXRpb25zLmFzaHg%3D&id=4","program_code":"3671","picture_code":"https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg","program_image":"","station_image":"Logo_Image_Logo20_img__8.jpg","program_id":"","timezone":"2"}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-05T22:05:37.000Z', - stop: '2022-03-05T22:27:12.000Z', - title: 'ארץ מולדת - בין תורכיה לבריטניה', - description: - "קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.", - icon: 'https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=kan.org.il + +const { parser, url } = require('./kan.org.il.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '19', + xmltv_id: 'KANEducational.il' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=19&day=06/03/2022' + ) +}) + +it('can parse response', () => { + const content = + '[{"title":"ארץ מולדת - בין תורכיה לבריטניה","start_time":"2022-03-06T00:05:37","end_time":"2022-03-06T00:27:12","id":"2598","age_category_desc":"0","epg_name":"ארץ מולדת","title1":"ארץ מולדת - בין תורכיה לבריטניה","chapter_number":"9","live_desc":"קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות\'מאני לבין תקוותיו מהבריטים הכובשים.","Station_Radio":"0","Station_Id":"20","stationUrlScheme":"kan11://plugin/?type=player&plugin_identifier=kan_player&ds=general-provider%3A%2F%2FfetchData%3Ftype%3DFEED_JSON%26url%3DaHR0cHM6Ly93d3cua2FuLm9yZy5pbC9hcHBLYW4vbGl2ZVN0YXRpb25zLmFzaHg%3D&id=4","program_code":"3671","picture_code":"https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg","program_image":"","station_image":"Logo_Image_Logo20_img__8.jpg","program_id":"","timezone":"2"}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-05T22:05:37.000Z', + stop: '2022-03-05T22:27:12.000Z', + title: 'ארץ מולדת - בין תורכיה לבריטניה', + description: + "קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.", + icon: 'https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/knr.gl/knr.gl.config.js b/sites/knr.gl/knr.gl.config.js index c9ca8fe2c..fc5492f82 100644 --- a/sites/knr.gl/knr.gl.config.js +++ b/sites/knr.gl/knr.gl.config.js @@ -1,65 +1,65 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'knr.gl', - days: 2, - url({ date }) { - return `https://knr.gl/admin/knr/TV/program/${date.format('YYYY-MM-DD')}/gl` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const start = parseStart(item, date) - const stop = start.add(1, 'h') - if (prev) prev.stop = start - programs.push({ - title: item.title, - description: item.description, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'America/Godthab') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data.program_list) return [] - const $ = cheerio.load(data.program_list) - const items = [] - $('dt').each(function () { - const titleElem = $(this) - .contents() - .filter(function () { - return this.nodeType === 3 - })[0] - items.push({ - title: titleElem.nodeValue.trim(), - description: $(this) - .next('dd') - .text() - .replace(/(\r\n|\n|\r)/gm, ' '), - time: $(this, 'strong').text().trim() - }) - }) - - return items -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'knr.gl', + days: 2, + url({ date }) { + return `https://knr.gl/admin/knr/TV/program/${date.format('YYYY-MM-DD')}/gl` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const start = parseStart(item, date) + const stop = start.add(1, 'h') + if (prev) prev.stop = start + programs.push({ + title: item.title, + description: item.description, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'America/Godthab') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data.program_list) return [] + const $ = cheerio.load(data.program_list) + const items = [] + $('dt').each(function () { + const titleElem = $(this) + .contents() + .filter(function () { + return this.nodeType === 3 + })[0] + items.push({ + title: titleElem.nodeValue.trim(), + description: $(this) + .next('dd') + .text() + .replace(/(\r\n|\n|\r)/gm, ' '), + time: $(this, 'strong').text().trim() + }) + }) + + return items +} diff --git a/sites/knr.gl/knr.gl.test.js b/sites/knr.gl/knr.gl.test.js index a0cb8cd4a..8c0a50b75 100644 --- a/sites/knr.gl/knr.gl.test.js +++ b/sites/knr.gl/knr.gl.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=knr.gl - -const { parser, url } = require('./knr.gl.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KNRTV.gl' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://knr.gl/admin/knr/TV/program/2021-11-22/gl') -}) - -it('can parse response', () => { - const content = - '{"program_list":"\\u003Cdt class=\\u0022program\\u0022\\u003E\\u003Cstrong\\u003E08:00\\u003C\\/strong\\u003E Meeqqanut - Toqqorsivimmiit\\u003C\\/dt\\u003E\\u003Cdt class=\\u0022knr-program-pointer knr-program-togle-program\\u0022 data-program-id=\\u0022588574\\u0022 data-module-path=\\u0022sites\\/knr\\/modules\\/custom\\/knr_site\\u0022\\u003E\\u003Cimg height=\\u00229\\u0022 width=\\u00229\\u0022 id=\\u0022icon_588574\\u0022 alt=\\u0022View description\\u0022 src=\\u0022\\/sites\\/knr\\/modules\\/custom\\/knr_site\\/assets\\/img\\/plus.gif\\u0022\\u003E\\u003Cstrong\\u003E08:30\\u003C\\/strong\\u003E ICC 2018 Piorsarsimassutikkut pisut (1:3)\\u003C\\/dt\\u003E\\u003Cdd id=\\u0022program_588574\\u0022 style=\\u0022display: none;\\u0022\\u003E\\u003Cdiv class=\\u0022box\\u0022\\u003E2018 ICC ataatsimersuareernerata kingorna unnukkut piorsarsimassutsikkut pisut takutinneqarput. Aammalu illoqarfik Utqiagvik ilisaritinneqarluni. Ove Heilmann, Aannguaq Nielsen, Aannguaq Reimer-Johansen\\r\\nKNR 09.12.2018\\u003C\\/div\\u003E\\u003C\\/dd\\u003E"}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-22T11:00:00.000Z', - stop: '2021-11-22T11:30:00.000Z', - title: 'Meeqqanut - Toqqorsivimmiit' - }, - { - start: '2021-11-22T11:30:00.000Z', - stop: '2021-11-22T12:30:00.000Z', - title: 'ICC 2018 Piorsarsimassutikkut pisut (1:3)', - description: - '2018 ICC ataatsimersuareernerata kingorna unnukkut piorsarsimassutsikkut pisut takutinneqarput. Aammalu illoqarfik Utqiagvik ilisaritinneqarluni. Ove Heilmann, Aannguaq Nielsen, Aannguaq Reimer-Johansen KNR 09.12.2018' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"program_list":""}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=knr.gl + +const { parser, url } = require('./knr.gl.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KNRTV.gl' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://knr.gl/admin/knr/TV/program/2021-11-22/gl') +}) + +it('can parse response', () => { + const content = + '{"program_list":"\\u003Cdt class=\\u0022program\\u0022\\u003E\\u003Cstrong\\u003E08:00\\u003C\\/strong\\u003E Meeqqanut - Toqqorsivimmiit\\u003C\\/dt\\u003E\\u003Cdt class=\\u0022knr-program-pointer knr-program-togle-program\\u0022 data-program-id=\\u0022588574\\u0022 data-module-path=\\u0022sites\\/knr\\/modules\\/custom\\/knr_site\\u0022\\u003E\\u003Cimg height=\\u00229\\u0022 width=\\u00229\\u0022 id=\\u0022icon_588574\\u0022 alt=\\u0022View description\\u0022 src=\\u0022\\/sites\\/knr\\/modules\\/custom\\/knr_site\\/assets\\/img\\/plus.gif\\u0022\\u003E\\u003Cstrong\\u003E08:30\\u003C\\/strong\\u003E ICC 2018 Piorsarsimassutikkut pisut (1:3)\\u003C\\/dt\\u003E\\u003Cdd id=\\u0022program_588574\\u0022 style=\\u0022display: none;\\u0022\\u003E\\u003Cdiv class=\\u0022box\\u0022\\u003E2018 ICC ataatsimersuareernerata kingorna unnukkut piorsarsimassutsikkut pisut takutinneqarput. Aammalu illoqarfik Utqiagvik ilisaritinneqarluni. Ove Heilmann, Aannguaq Nielsen, Aannguaq Reimer-Johansen\\r\\nKNR 09.12.2018\\u003C\\/div\\u003E\\u003C\\/dd\\u003E"}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-22T11:00:00.000Z', + stop: '2021-11-22T11:30:00.000Z', + title: 'Meeqqanut - Toqqorsivimmiit' + }, + { + start: '2021-11-22T11:30:00.000Z', + stop: '2021-11-22T12:30:00.000Z', + title: 'ICC 2018 Piorsarsimassutikkut pisut (1:3)', + description: + '2018 ICC ataatsimersuareernerata kingorna unnukkut piorsarsimassutsikkut pisut takutinneqarput. Aammalu illoqarfik Utqiagvik ilisaritinneqarluni. Ove Heilmann, Aannguaq Nielsen, Aannguaq Reimer-Johansen KNR 09.12.2018' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"program_list":""}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/kplus.vn/kplus.vn.config.js b/sites/kplus.vn/kplus.vn.config.js index a134f1cd4..9ed52fd7f 100644 --- a/sites/kplus.vn/kplus.vn.config.js +++ b/sites/kplus.vn/kplus.vn.config.js @@ -1,79 +1,79 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(timezone) -dayjs.extend(utc) - -const API_ENDPOINT = 'https://www.kplus.vn/Schedule/getSchedule' - -module.exports = { - site: 'kplus.vn', - days: 2, - url: API_ENDPOINT, - request: { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data({ date }) { - const params = new URLSearchParams() - params.append('date', date.format('D-M-YYYY')) - params.append('categories', '') - - return params - }, - method: 'POST' - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - const start = parseStart(item) - const stop = start.add(1, 'h') - if (prev) prev.stop = start - programs.push({ - title: item.Program.Name, - icon: item.Program.Images, - category: item.Program.Genres, - start, - stop - }) - }) - - return programs - }, - async channels() { - const params = new URLSearchParams() - params.append('date', dayjs().format('D-M-YYYY')) - params.append('categories', '') - const data = await axios - .post(API_ENDPOINT, params, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }) - .then(r => r.data) - .catch(console.log) - - return data.Channels.map(item => { - return { - lang: 'vi', - site_id: item.Id, - name: item.Name - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.ShowingTime, 'Asia/Ho_Chi_Minh') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.Schedules)) return [] - - return data.Schedules.filter(i => i.ChannelId == channel.site_id) -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(timezone) +dayjs.extend(utc) + +const API_ENDPOINT = 'https://www.kplus.vn/Schedule/getSchedule' + +module.exports = { + site: 'kplus.vn', + days: 2, + url: API_ENDPOINT, + request: { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data({ date }) { + const params = new URLSearchParams() + params.append('date', date.format('D-M-YYYY')) + params.append('categories', '') + + return params + }, + method: 'POST' + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + const start = parseStart(item) + const stop = start.add(1, 'h') + if (prev) prev.stop = start + programs.push({ + title: item.Program.Name, + icon: item.Program.Images, + category: item.Program.Genres, + start, + stop + }) + }) + + return programs + }, + async channels() { + const params = new URLSearchParams() + params.append('date', dayjs().format('D-M-YYYY')) + params.append('categories', '') + const data = await axios + .post(API_ENDPOINT, params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + .then(r => r.data) + .catch(console.log) + + return data.Channels.map(item => { + return { + lang: 'vi', + site_id: item.Id, + name: item.Name + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.ShowingTime, 'Asia/Ho_Chi_Minh') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.Schedules)) return [] + + return data.Schedules.filter(i => i.ChannelId == channel.site_id) +} diff --git a/sites/kplus.vn/kplus.vn.test.js b/sites/kplus.vn/kplus.vn.test.js index ddadd4470..e6a359f45 100644 --- a/sites/kplus.vn/kplus.vn.test.js +++ b/sites/kplus.vn/kplus.vn.test.js @@ -1,67 +1,67 @@ -// npm run channels:parse -- --config=sites/kplus.vn/kplus.vn.config.js --output=sites/kplus.vn/kplus.vn.channels.xml -// npm run grab -- --site=kplus.vn - -const { parser, url, request } = require('./kplus.vn.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '7019', - xmltv_id: 'KPlus1HD.vn' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.kplus.vn/Schedule/getSchedule') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ date }) - - expect(data.get('date')).toBe('15-3-2022') - expect(data.get('categories')).toBe('') -}) - -it('can parse response', () => { - const content = - '{"SchedulesCount":1105,"ChannelsCount":28,"Schedules":[{"Id":12195,"ChannelId":7019,"ProgramId":35111026,"EpgProgramId":"1252496\\r","ShowingTime":"2022-03-15T06:15:00","EpgBroadcastId":"HD_ENT_DOC_LNO_21_2649421_2652183_4383385_OnAir","EpgId":"HD_ENT_DOC_LNO_21_2649421_2652183_4383385_OnAir","IsDeleted":false,"CreatedOn":"2022-03-15T06:22:45","UpdatedOn":"0001-01-01T00:00:00","Channel":{"Id":7019,"Name":"K+1 HD","Image":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","LiveUrlSegment":"highlights/broadcast-schedule/K-1-HD","FeatureImage":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","EpgId":null,"IsOTTEnabled":false,"StartOver":0,"DisplayOrder":0},"Program":{"Id":35111026,"Name":"WEEKLY FILMS AND STARS, EP740","BodyContent":"","Cast":"","Director":"","Duration":0,"EpgId":"93701","EpgProgramId":null,"Episode":0,"Genres":"Documentary","Images":"https://img.kplus.vn/images?filename=Media/HDVN/2022_02/ENT_DOC_LNO_21_2649421_2652183_2652183.jpg","IsFeatured":false,"IsOTTEnabled":true,"IsRebroadcast":false,"ShortDescription":"","SubTitle":"","Trailers":"","UrlSegment":"highlights/broadcast-schedule/93701/weekly-films-and-stars-ep740","CreatedOn":"2022-03-16T00:15:45","UpdatedOn":"2022-03-16T00:15:45","ParentalRating":null},"RelatedSchedules":null},{"Id":12196,"ChannelId":7019,"ProgramId":35111279,"EpgProgramId":"798685\\r","ShowingTime":"2022-03-15T07:00:00","EpgBroadcastId":"HD_MOV_COM__2632318_4383386_OnAir","EpgId":"HD_MOV_COM__2632318_4383386_OnAir","IsDeleted":false,"CreatedOn":"2022-03-15T07:02:46","UpdatedOn":"0001-01-01T00:00:00","Channel":{"Id":7019,"Name":"K+1 HD","Image":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","LiveUrlSegment":"highlights/broadcast-schedule/K-1-HD","FeatureImage":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","EpgId":null,"IsOTTEnabled":false,"StartOver":0,"DisplayOrder":0},"Program":{"Id":35111279,"Name":"ST. VINCENT","BodyContent":"","Cast":"Bill Murray, Melissa McCarthy, Naomi Watts","Director":"Theodore Melfi","Duration":0,"EpgId":"93959","EpgProgramId":null,"Episode":0,"Genres":"Comedy","Images":"https://img.kplus.vn/images?filename=Media/HDVN/2020_05/MOV_COM__2632318_2632318.jpg","IsFeatured":false,"IsOTTEnabled":true,"IsRebroadcast":false,"ShortDescription":"","SubTitle":"","Trailers":"","UrlSegment":"highlights/broadcast-schedule/93959/st-vincent","CreatedOn":"2022-03-16T00:15:45","UpdatedOn":"2022-03-16T00:15:45","ParentalRating":null},"RelatedSchedules":null}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-14T23:15:00.000Z', - stop: '2022-03-15T00:00:00.000Z', - title: 'WEEKLY FILMS AND STARS, EP740', - icon: 'https://img.kplus.vn/images?filename=Media/HDVN/2022_02/ENT_DOC_LNO_21_2649421_2652183_2652183.jpg', - category: 'Documentary' - }, - { - start: '2022-03-15T00:00:00.000Z', - stop: '2022-03-15T01:00:00.000Z', - title: 'ST. VINCENT', - icon: 'https://img.kplus.vn/images?filename=Media/HDVN/2020_05/MOV_COM__2632318_2632318.jpg', - category: 'Comedy' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"SchedulesCount":0,"ChannelsCount":0,"Schedules":[],"Channels":[],"MinDuration":0}', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/kplus.vn/kplus.vn.config.js --output=sites/kplus.vn/kplus.vn.channels.xml +// npm run grab -- --site=kplus.vn + +const { parser, url, request } = require('./kplus.vn.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '7019', + xmltv_id: 'KPlus1HD.vn' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.kplus.vn/Schedule/getSchedule') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ date }) + + expect(data.get('date')).toBe('15-3-2022') + expect(data.get('categories')).toBe('') +}) + +it('can parse response', () => { + const content = + '{"SchedulesCount":1105,"ChannelsCount":28,"Schedules":[{"Id":12195,"ChannelId":7019,"ProgramId":35111026,"EpgProgramId":"1252496\\r","ShowingTime":"2022-03-15T06:15:00","EpgBroadcastId":"HD_ENT_DOC_LNO_21_2649421_2652183_4383385_OnAir","EpgId":"HD_ENT_DOC_LNO_21_2649421_2652183_4383385_OnAir","IsDeleted":false,"CreatedOn":"2022-03-15T06:22:45","UpdatedOn":"0001-01-01T00:00:00","Channel":{"Id":7019,"Name":"K+1 HD","Image":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","LiveUrlSegment":"highlights/broadcast-schedule/K-1-HD","FeatureImage":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","EpgId":null,"IsOTTEnabled":false,"StartOver":0,"DisplayOrder":0},"Program":{"Id":35111026,"Name":"WEEKLY FILMS AND STARS, EP740","BodyContent":"","Cast":"","Director":"","Duration":0,"EpgId":"93701","EpgProgramId":null,"Episode":0,"Genres":"Documentary","Images":"https://img.kplus.vn/images?filename=Media/HDVN/2022_02/ENT_DOC_LNO_21_2649421_2652183_2652183.jpg","IsFeatured":false,"IsOTTEnabled":true,"IsRebroadcast":false,"ShortDescription":"","SubTitle":"","Trailers":"","UrlSegment":"highlights/broadcast-schedule/93701/weekly-films-and-stars-ep740","CreatedOn":"2022-03-16T00:15:45","UpdatedOn":"2022-03-16T00:15:45","ParentalRating":null},"RelatedSchedules":null},{"Id":12196,"ChannelId":7019,"ProgramId":35111279,"EpgProgramId":"798685\\r","ShowingTime":"2022-03-15T07:00:00","EpgBroadcastId":"HD_MOV_COM__2632318_4383386_OnAir","EpgId":"HD_MOV_COM__2632318_4383386_OnAir","IsDeleted":false,"CreatedOn":"2022-03-15T07:02:46","UpdatedOn":"0001-01-01T00:00:00","Channel":{"Id":7019,"Name":"K+1 HD","Image":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","LiveUrlSegment":"highlights/broadcast-schedule/K-1-HD","FeatureImage":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","EpgId":null,"IsOTTEnabled":false,"StartOver":0,"DisplayOrder":0},"Program":{"Id":35111279,"Name":"ST. VINCENT","BodyContent":"","Cast":"Bill Murray, Melissa McCarthy, Naomi Watts","Director":"Theodore Melfi","Duration":0,"EpgId":"93959","EpgProgramId":null,"Episode":0,"Genres":"Comedy","Images":"https://img.kplus.vn/images?filename=Media/HDVN/2020_05/MOV_COM__2632318_2632318.jpg","IsFeatured":false,"IsOTTEnabled":true,"IsRebroadcast":false,"ShortDescription":"","SubTitle":"","Trailers":"","UrlSegment":"highlights/broadcast-schedule/93959/st-vincent","CreatedOn":"2022-03-16T00:15:45","UpdatedOn":"2022-03-16T00:15:45","ParentalRating":null},"RelatedSchedules":null}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-14T23:15:00.000Z', + stop: '2022-03-15T00:00:00.000Z', + title: 'WEEKLY FILMS AND STARS, EP740', + icon: 'https://img.kplus.vn/images?filename=Media/HDVN/2022_02/ENT_DOC_LNO_21_2649421_2652183_2652183.jpg', + category: 'Documentary' + }, + { + start: '2022-03-15T00:00:00.000Z', + stop: '2022-03-15T01:00:00.000Z', + title: 'ST. VINCENT', + icon: 'https://img.kplus.vn/images?filename=Media/HDVN/2020_05/MOV_COM__2632318_2632318.jpg', + category: 'Comedy' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"SchedulesCount":0,"ChannelsCount":0,"Schedules":[],"Channels":[],"MinDuration":0}', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/kvf.fo/kvf.fo.config.js b/sites/kvf.fo/kvf.fo.config.js index f95a27b98..f53814122 100644 --- a/sites/kvf.fo/kvf.fo.config.js +++ b/sites/kvf.fo/kvf.fo.config.js @@ -1,71 +1,71 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'kvf.fo', - days: 2, - url({ date }) { - return `https://kvf.fo/nskra/sv?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev && start.isBefore(prev.stop)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const string = $item('.s-normal > .s-time1').text().trim() - let [time] = string.match(/^(\d{2}:\d{2})/g) || [null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') -} - -function parseStop($item, date) { - const string = $item('.s-normal > .s-time1').text().trim() - let [time] = string.match(/(\d{2}:\d{2})$/g) || [null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') -} - -function parseTitle($item) { - return $item('.s-normal > .s-heiti').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.view > .view-content > div.views-row').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'kvf.fo', + days: 2, + url({ date }) { + return `https://kvf.fo/nskra/sv?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev && start.isBefore(prev.stop)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const string = $item('.s-normal > .s-time1').text().trim() + let [time] = string.match(/^(\d{2}:\d{2})/g) || [null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') +} + +function parseStop($item, date) { + const string = $item('.s-normal > .s-time1').text().trim() + let [time] = string.match(/(\d{2}:\d{2})$/g) || [null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') +} + +function parseTitle($item) { + return $item('.s-normal > .s-heiti').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.view > .view-content > div.views-row').toArray() +} diff --git a/sites/kvf.fo/kvf.fo.test.js b/sites/kvf.fo/kvf.fo.test.js index 637ea272b..bf61455f7 100644 --- a/sites/kvf.fo/kvf.fo.test.js +++ b/sites/kvf.fo/kvf.fo.test.js @@ -1,44 +1,44 @@ -// npm run grab -- --site=kvf.fo - -const { parser, url } = require('./kvf.fo.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KVFSjonvarp.fo' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://kvf.fo/nskra/sv?date=2021-11-21') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, './__data__/example.html')) - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result[2]).toMatchObject({ - start: '2021-11-21T18:05:00.000Z', - stop: '2021-11-21T18:30:00.000Z', - title: 'Letibygd 13' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=kvf.fo + +const { parser, url } = require('./kvf.fo.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KVFSjonvarp.fo' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://kvf.fo/nskra/sv?date=2021-11-21') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, './__data__/example.html')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result[2]).toMatchObject({ + start: '2021-11-21T18:05:00.000Z', + stop: '2021-11-21T18:30:00.000Z', + title: 'Letibygd 13' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ' ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/m.tv.sms.cz/m.tv.sms.cz.config.js b/sites/m.tv.sms.cz/m.tv.sms.cz.config.js index fadfd6860..de1c4d987 100644 --- a/sites/m.tv.sms.cz/m.tv.sms.cz.config.js +++ b/sites/m.tv.sms.cz/m.tv.sms.cz.config.js @@ -1,60 +1,60 @@ -const cheerio = require('cheerio') -const iconv = require('iconv-lite') -const { DateTime } = require('luxon') - -module.exports = { - site: 'm.tv.sms.cz', - days: 2, - url: function ({ date, channel }) { - return `https://m.tv.sms.cz/index.php?stanice=${channel.site_id}&cas=0&den=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ buffer, date }) { - const programs = [] - const items = parseItems(buffer) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('div > span').text().trim() - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Prague' }).toUTC() -} - -function parseDescription($item) { - return $item('a.nazev > div.detail').text().trim() -} - -function parseTitle($item) { - return $item('a.nazev > div:nth-child(1)').text().trim() -} - -function parseItems(buffer) { - const string = iconv.decode(buffer, 'win1250') - const $ = cheerio.load(string) - - return $('#obsah > div > div.porady > div.porad').toArray() -} +const cheerio = require('cheerio') +const iconv = require('iconv-lite') +const { DateTime } = require('luxon') + +module.exports = { + site: 'm.tv.sms.cz', + days: 2, + url: function ({ date, channel }) { + return `https://m.tv.sms.cz/index.php?stanice=${channel.site_id}&cas=0&den=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ buffer, date }) { + const programs = [] + const items = parseItems(buffer) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('div > span').text().trim() + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Prague' }).toUTC() +} + +function parseDescription($item) { + return $item('a.nazev > div.detail').text().trim() +} + +function parseTitle($item) { + return $item('a.nazev > div:nth-child(1)').text().trim() +} + +function parseItems(buffer) { + const string = iconv.decode(buffer, 'win1250') + const $ = cheerio.load(string) + + return $('#obsah > div > div.porady > div.porad').toArray() +} diff --git a/sites/m.tv.sms.cz/m.tv.sms.cz.test.js b/sites/m.tv.sms.cz/m.tv.sms.cz.test.js index 9346a5af2..6ce326a3d 100644 --- a/sites/m.tv.sms.cz/m.tv.sms.cz.test.js +++ b/sites/m.tv.sms.cz/m.tv.sms.cz.test.js @@ -1,59 +1,59 @@ -// npm run grab -- --site=m.tv.sms.cz - -const { parser, url } = require('./m.tv.sms.cz.config.js') -const iconv = require('iconv-lite') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Cero', - xmltv_id: '0.es' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://m.tv.sms.cz/index.php?stanice=Cero&cas=0&den=2023-06-11' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const buffer = iconv.encode(content, 'win1250') - const results = parser({ buffer, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-11T03:21:00.000Z', - stop: '2023-06-11T04:08:00.000Z', - title: 'Conspiraciones al descubierto: La bomba atómica alemana y el hundimiento del Titanic', - description: 'Documentales' - }) - - expect(results[25]).toMatchObject({ - start: '2023-06-12T02:23:00.000Z', - stop: '2023-06-12T03:23:00.000Z', - title: 'Rapa I (6)', - description: 'Series' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - buffer: iconv.encode( - Buffer.from( - '' - ), - 'win1250' - ) - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=m.tv.sms.cz + +const { parser, url } = require('./m.tv.sms.cz.config.js') +const iconv = require('iconv-lite') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Cero', + xmltv_id: '0.es' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://m.tv.sms.cz/index.php?stanice=Cero&cas=0&den=2023-06-11' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const buffer = iconv.encode(content, 'win1250') + const results = parser({ buffer, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-11T03:21:00.000Z', + stop: '2023-06-11T04:08:00.000Z', + title: 'Conspiraciones al descubierto: La bomba atómica alemana y el hundimiento del Titanic', + description: 'Documentales' + }) + + expect(results[25]).toMatchObject({ + start: '2023-06-12T02:23:00.000Z', + stop: '2023-06-12T03:23:00.000Z', + title: 'Rapa I (6)', + description: 'Series' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + buffer: iconv.encode( + Buffer.from( + '' + ), + 'win1250' + ) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/magentatv.at/magentatv.at.config.js b/sites/magentatv.at/magentatv.at.config.js index 71f16b0e9..502351968 100644 --- a/sites/magentatv.at/magentatv.at.config.js +++ b/sites/magentatv.at/magentatv.at.config.js @@ -1,130 +1,130 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_STATIC_ENDPOINT = 'https://static.spark.magentatv.at/deu/web/epg-service-lite/at' -const API_PROD_ENDPOINT = 'https://prod.spark.magentatv.at/deu/web/linear-service/v2' - -module.exports = { - site: 'magentatv.at', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `${API_STATIC_ENDPOINT}/de/events/segments/${date.format('YYYYMMDDHHmmss')}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const promises = [ - axios.get( - `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(6, 'h').format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(12, 'h').format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(18, 'h').format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ) - ] - - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - const parsed = parseItems(r.value.data, channel) - - items = items.concat(parsed) - } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item) - programs.push({ - title: item.title, - sub_title: detail.episodeName, - description: detail.longDescription, - date: detail.productionDate, - category: detail.genres, - actors: detail.actors, - directors: detail.directors, - producers: detail.producers, - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_PROD_ENDPOINT}/channels?cityId=65535&language=de&productClass=Orion-DASH`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'de', - site_id: item.id, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.id) return {} - const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.entries)) return [] - const channelData = data.entries.find(e => e.channelId === channel.site_id) - if (!channelData) return [] - - return Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(detail) { - if (!detail.seasonNumber) return null - if (String(detail.seasonNumber).length > 2) return null - return detail.seasonNumber -} - -function parseEpisode(detail) { - if (!detail.episodeNumber) return null - if (String(detail.episodeNumber).length > 3) return null - return detail.episodeNumber -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_STATIC_ENDPOINT = 'https://static.spark.magentatv.at/deu/web/epg-service-lite/at' +const API_PROD_ENDPOINT = 'https://prod.spark.magentatv.at/deu/web/linear-service/v2' + +module.exports = { + site: 'magentatv.at', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `${API_STATIC_ENDPOINT}/de/events/segments/${date.format('YYYYMMDDHHmmss')}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const promises = [ + axios.get( + `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(6, 'h').format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(12, 'h').format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(18, 'h').format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ) + ] + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.title, + sub_title: detail.episodeName, + description: detail.longDescription, + date: detail.productionDate, + category: detail.genres, + actors: detail.actors, + directors: detail.directors, + producers: detail.producers, + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_PROD_ENDPOINT}/channels?cityId=65535&language=de&productClass=Orion-DASH`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'de', + site_id: item.id, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.id) return {} + const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.entries)) return [] + const channelData = data.entries.find(e => e.channelId === channel.site_id) + if (!channelData) return [] + + return Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(detail) { + if (!detail.seasonNumber) return null + if (String(detail.seasonNumber).length > 2) return null + return detail.seasonNumber +} + +function parseEpisode(detail) { + if (!detail.episodeNumber) return null + if (String(detail.episodeNumber).length > 3) return null + return detail.episodeNumber +} diff --git a/sites/magentatv.at/magentatv.at.test.js b/sites/magentatv.at/magentatv.at.test.js index de0c538a9..bebf091e2 100644 --- a/sites/magentatv.at/magentatv.at.test.js +++ b/sites/magentatv.at/magentatv.at.test.js @@ -1,97 +1,97 @@ -// npm run channels:parse -- --config=./sites/magentatv.at/magentatv.at.config.js --output=./sites/magentatv.at/magentatv.at.channels.xml -// npm run grab -- --site=magentatv.at - -const { parser, url } = require('./magentatv.at.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const API_STATIC_ENDPOINT = 'https://static.spark.magentatv.at/deu/web/epg-service-lite/at' -const API_PROD_ENDPOINT = 'https://prod.spark.magentatv.at/deu/web/linear-service/v2' - -jest.mock('axios') - -const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '13TH_STREET_HD', - xmltv_id: '13thStreet.de', - lang: 'de' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe(`${API_STATIC_ENDPOINT}/de/events/segments/20221030000000`) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030060000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030120000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030180000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) - }) - } else if ( - url === - `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2236391~~2FEP019388320252,imi:af4af994f29354e64878101c0612b17999d0c1a3?returnLinearContent=true` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T23:55:00.000Z', - stop: '2022-10-30T01:40:00.000Z', - title: 'Law & Order: Special Victims Unit', - sub_title: 'Mutterinstinkt', - description: - 'Patty Branson wird von einem Jungen in einem Park angegriffen und von diesem verfolgt. Der Junge wurde von Michelle Osborne engagiert, die vorgibt, die leibliche Mutter des Mädchens zu sein. Doch ist dies tatsächlich die Wahrheit?', - date: '2004', - category: ['Drama-Serie', 'Krimi Drama', 'Action', 'Thriller'], - actors: [ - 'Christopher Meloni', - 'Mariska Hargitay', - 'Richard Belzer', - 'Dann Florek', - 'Ice-T', - 'BD Wong', - 'Diane Neal', - 'Tamara Tunie', - 'Abigail Breslin', - 'Lea Thompson' - ], - directors: ['Arthur W. Forney'], - producers: ['Dick Wolf', 'Ted Kotcheff', 'Neal Baer'], - season: 6, - episode: 1 - }) -}) - -it('can handle empty guide', async () => { - let results = await parser({ content: '', channel, date }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/magentatv.at/magentatv.at.config.js --output=./sites/magentatv.at/magentatv.at.channels.xml +// npm run grab -- --site=magentatv.at + +const { parser, url } = require('./magentatv.at.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const API_STATIC_ENDPOINT = 'https://static.spark.magentatv.at/deu/web/epg-service-lite/at' +const API_PROD_ENDPOINT = 'https://prod.spark.magentatv.at/deu/web/linear-service/v2' + +jest.mock('axios') + +const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '13TH_STREET_HD', + xmltv_id: '13thStreet.de', + lang: 'de' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe(`${API_STATIC_ENDPOINT}/de/events/segments/20221030000000`) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030060000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030120000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030180000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) + }) + } else if ( + url === + `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2236391~~2FEP019388320252,imi:af4af994f29354e64878101c0612b17999d0c1a3?returnLinearContent=true` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T23:55:00.000Z', + stop: '2022-10-30T01:40:00.000Z', + title: 'Law & Order: Special Victims Unit', + sub_title: 'Mutterinstinkt', + description: + 'Patty Branson wird von einem Jungen in einem Park angegriffen und von diesem verfolgt. Der Junge wurde von Michelle Osborne engagiert, die vorgibt, die leibliche Mutter des Mädchens zu sein. Doch ist dies tatsächlich die Wahrheit?', + date: '2004', + category: ['Drama-Serie', 'Krimi Drama', 'Action', 'Thriller'], + actors: [ + 'Christopher Meloni', + 'Mariska Hargitay', + 'Richard Belzer', + 'Dann Florek', + 'Ice-T', + 'BD Wong', + 'Diane Neal', + 'Tamara Tunie', + 'Abigail Breslin', + 'Lea Thompson' + ], + directors: ['Arthur W. Forney'], + producers: ['Dick Wolf', 'Ted Kotcheff', 'Neal Baer'], + season: 6, + episode: 1 + }) +}) + +it('can handle empty guide', async () => { + let results = await parser({ content: '', channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/magentatv.de/magentatv.de.config.js b/sites/magentatv.de/magentatv.de.config.js index 4edefacc2..ad7f191a2 100644 --- a/sites/magentatv.de/magentatv.de.config.js +++ b/sites/magentatv.de/magentatv.de.config.js @@ -1,123 +1,123 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const X_CSRFTOKEN = 'e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe' -const COOKIE = - 'JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6; CSESSIONID=4A36799EF09D80539BBA8E8211FA80D3; CSRFSESSION=e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe; JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6' - -module.exports = { - site: 'magentatv.de', - days: 2, - url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList', - request: { - method: 'POST', - headers: { - X_CSRFToken: X_CSRFTOKEN, - 'Content-Type': 'application/json', - Cookie: COOKIE - }, - data({ channel, date }) { - return { - count: -1, - isFillProgram: 1, - offset: 0, - properties: [ - { - include: 'endtime,genres,id,name,starttime,channelid,pictures,introduce', - name: 'playbill' - } - ], - type: 2, - begintime: date.format('YYYYMMDD000000'), - channelid: channel.site_id, - endtime: date.add(1, 'd').format('YYYYMMDD000000') - } - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.introduce, - icon: parseIcon(item), - category: parseCategory(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .post( - 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel', - { - channelNamespace: 2, - filterlist: [ - { - key: 'IsHide', - value: '-1' - } - ], - metaDataVer: 'Channel/1.1', - properties: [ - { - include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name', - name: 'logicalChannel' - } - ], - returnSatChannel: 0 - }, - { - headers: { - X_CSRFToken: X_CSRFTOKEN, - 'Content-Type': 'application/json', - Cookie: COOKIE - } - } - ) - .then(r => r.data) - .catch(console.log) - - return data.channellist.map(item => { - return { - lang: 'de', - site_id: item.contentId, - name: item.name - } - }) - } -} - -function parseCategory(item) { - return item.genres - ? item.genres - .replace('und', ',') - .split(',') - .map(i => i.trim()) - : [] -} - -function parseIcon(item) { - if (!Array.isArray(item.pictures) || !item.pictures.length) return null - - return item.pictures[0].href -} - -function parseStart(item) { - return dayjs(item.starttime) -} - -function parseStop(item) { - return dayjs(item.endtime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.playbilllist)) return [] - - return data.playbilllist -} +const axios = require('axios') +const dayjs = require('dayjs') + +const X_CSRFTOKEN = 'e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe' +const COOKIE = + 'JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6; CSESSIONID=4A36799EF09D80539BBA8E8211FA80D3; CSRFSESSION=e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe; JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6' + +module.exports = { + site: 'magentatv.de', + days: 2, + url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList', + request: { + method: 'POST', + headers: { + X_CSRFToken: X_CSRFTOKEN, + 'Content-Type': 'application/json', + Cookie: COOKIE + }, + data({ channel, date }) { + return { + count: -1, + isFillProgram: 1, + offset: 0, + properties: [ + { + include: 'endtime,genres,id,name,starttime,channelid,pictures,introduce', + name: 'playbill' + } + ], + type: 2, + begintime: date.format('YYYYMMDD000000'), + channelid: channel.site_id, + endtime: date.add(1, 'd').format('YYYYMMDD000000') + } + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.introduce, + icon: parseIcon(item), + category: parseCategory(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .post( + 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel', + { + channelNamespace: 2, + filterlist: [ + { + key: 'IsHide', + value: '-1' + } + ], + metaDataVer: 'Channel/1.1', + properties: [ + { + include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name', + name: 'logicalChannel' + } + ], + returnSatChannel: 0 + }, + { + headers: { + X_CSRFToken: X_CSRFTOKEN, + 'Content-Type': 'application/json', + Cookie: COOKIE + } + } + ) + .then(r => r.data) + .catch(console.log) + + return data.channellist.map(item => { + return { + lang: 'de', + site_id: item.contentId, + name: item.name + } + }) + } +} + +function parseCategory(item) { + return item.genres + ? item.genres + .replace('und', ',') + .split(',') + .map(i => i.trim()) + : [] +} + +function parseIcon(item) { + if (!Array.isArray(item.pictures) || !item.pictures.length) return null + + return item.pictures[0].href +} + +function parseStart(item) { + return dayjs(item.starttime) +} + +function parseStop(item) { + return dayjs(item.endtime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.playbilllist)) return [] + + return data.playbilllist +} diff --git a/sites/magentatv.de/magentatv.de.test.js b/sites/magentatv.de/magentatv.de.test.js index 42a30a118..9fbf8bf93 100644 --- a/sites/magentatv.de/magentatv.de.test.js +++ b/sites/magentatv.de/magentatv.de.test.js @@ -1,79 +1,79 @@ -// npm run channels:parse -- --config=./sites/magentatv.de/magentatv.de.config.js --output=./sites/magentatv.de/magentatv.de.channels.xml -// npm run grab -- --site=magentatv.de - -const { parser, url, request } = require('./magentatv.de.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-09', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '255', - xmltv_id: '13thStreet.de' -} - -it('can generate valid url', () => { - expect(url).toBe('https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - X_CSRFToken: 'e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe', - 'Content-Type': 'application/json', - Cookie: - 'JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6; CSESSIONID=4A36799EF09D80539BBA8E8211FA80D3; CSRFSESSION=e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe; JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - count: -1, - isFillProgram: 1, - offset: 0, - properties: [ - { - include: 'endtime,genres,id,name,starttime,channelid,pictures,introduce', - name: 'playbill' - } - ], - type: 2, - begintime: '20220309000000', - channelid: '255', - endtime: '20220310000000' - }) -}) - -it('can parse response', () => { - const content = - '{"playbilllist":[{"id":"30021745","name":"FBI: Special Crime Unit","introduce":"Nachdem ein Mann von einem Sprengstoffpaket getötet wurde, das zu ihm nach Hause geschickt wurde, versucht das Team, den Absender zu fassen und sein neuestes tödliches Paket abzufangen. Maggie hat Mühe, ihrer jüngeren Schwester zu vertrauen.","channelid":"255","starttime":"2022-03-09 01:00:00 UTC+01:00","endtime":"2022-03-09 01:45:00 UTC+01:00","genres":"Wissen,Natur und Tiere","pictures":[{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h9_af.jpg","description":"Brother\'s Keeper","imageType":"1","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1440","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/5/p15528073_i_h9_ae.jpg","description":"FBI","imageType":"13","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1440","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h8_af.jpg","description":"Brother\'s Keeper","imageType":"17","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1920","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/5/p15528073_i_h10_af.jpg","description":"FBI","imageType":"18","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1920","1080"]}]}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-09T00:00:00.000Z', - stop: '2022-03-09T00:45:00.000Z', - title: 'FBI: Special Crime Unit', - description: - 'Nachdem ein Mann von einem Sprengstoffpaket getötet wurde, das zu ihm nach Hause geschickt wurde, versucht das Team, den Absender zu fassen und sein neuestes tödliches Paket abzufangen. Maggie hat Mühe, ihrer jüngeren Schwester zu vertrauen.', - icon: 'http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h9_af.jpg', - category: ['Wissen', 'Natur', 'Tiere'] - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"counttotal":"0"}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/magentatv.de/magentatv.de.config.js --output=./sites/magentatv.de/magentatv.de.channels.xml +// npm run grab -- --site=magentatv.de + +const { parser, url, request } = require('./magentatv.de.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-09', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '255', + xmltv_id: '13thStreet.de' +} + +it('can generate valid url', () => { + expect(url).toBe('https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + X_CSRFToken: 'e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe', + 'Content-Type': 'application/json', + Cookie: + 'JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6; CSESSIONID=4A36799EF09D80539BBA8E8211FA80D3; CSRFSESSION=e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe; JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + count: -1, + isFillProgram: 1, + offset: 0, + properties: [ + { + include: 'endtime,genres,id,name,starttime,channelid,pictures,introduce', + name: 'playbill' + } + ], + type: 2, + begintime: '20220309000000', + channelid: '255', + endtime: '20220310000000' + }) +}) + +it('can parse response', () => { + const content = + '{"playbilllist":[{"id":"30021745","name":"FBI: Special Crime Unit","introduce":"Nachdem ein Mann von einem Sprengstoffpaket getötet wurde, das zu ihm nach Hause geschickt wurde, versucht das Team, den Absender zu fassen und sein neuestes tödliches Paket abzufangen. Maggie hat Mühe, ihrer jüngeren Schwester zu vertrauen.","channelid":"255","starttime":"2022-03-09 01:00:00 UTC+01:00","endtime":"2022-03-09 01:45:00 UTC+01:00","genres":"Wissen,Natur und Tiere","pictures":[{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h9_af.jpg","description":"Brother\'s Keeper","imageType":"1","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1440","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/5/p15528073_i_h9_ae.jpg","description":"FBI","imageType":"13","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1440","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h8_af.jpg","description":"Brother\'s Keeper","imageType":"17","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1920","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/5/p15528073_i_h10_af.jpg","description":"FBI","imageType":"18","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1920","1080"]}]}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-09T00:00:00.000Z', + stop: '2022-03-09T00:45:00.000Z', + title: 'FBI: Special Crime Unit', + description: + 'Nachdem ein Mann von einem Sprengstoffpaket getötet wurde, das zu ihm nach Hause geschickt wurde, versucht das Team, den Absender zu fassen und sein neuestes tödliches Paket abzufangen. Maggie hat Mühe, ihrer jüngeren Schwester zu vertrauen.', + icon: 'http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h9_af.jpg', + category: ['Wissen', 'Natur', 'Tiere'] + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"counttotal":"0"}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/magticom.ge/magticom.ge.config.js b/sites/magticom.ge/magticom.ge.config.js index 610444715..0e5a475d2 100644 --- a/sites/magticom.ge/magticom.ge.config.js +++ b/sites/magticom.ge/magticom.ge.config.js @@ -1,86 +1,86 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'magticom.ge', - days: 2, - url: 'https://www.magticom.ge/request/channel-program.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('channelId', channel.site_id) - params.append('start', date.unix()) - params.append('end', date.add(1, 'd').unix()) - - return params - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.info, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.magticom.ge/en/tv/tv-services/tv-guide') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const channels = $( - '#article > article > div > div > div.tv-guide > div.tv-guide-channels > div.tv-guide-channel' - ).toArray() - - return channels.map(item => { - const $item = cheerio.load(item) - const channelId = $item('*').data('id') - return { - lang: 'ka', - site_id: channelId, - name: $item('.tv-guide-channel-title > div > div').text() - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.startTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') -} - -function parseStop(item) { - return dayjs.tz(item.endTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch (error) { - console.log(error.message) - } - if (!data || !Array.isArray(data)) return [] - - return data -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'magticom.ge', + days: 2, + url: 'https://www.magticom.ge/request/channel-program.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('channelId', channel.site_id) + params.append('start', date.unix()) + params.append('end', date.add(1, 'd').unix()) + + return params + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.info, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.magticom.ge/en/tv/tv-services/tv-guide') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const channels = $( + '#article > article > div > div > div.tv-guide > div.tv-guide-channels > div.tv-guide-channel' + ).toArray() + + return channels.map(item => { + const $item = cheerio.load(item) + const channelId = $item('*').data('id') + return { + lang: 'ka', + site_id: channelId, + name: $item('.tv-guide-channel-title > div > div').text() + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.startTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') +} + +function parseStop(item) { + return dayjs.tz(item.endTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch (error) { + console.log(error.message) + } + if (!data || !Array.isArray(data)) return [] + + return data +} diff --git a/sites/magticom.ge/magticom.ge.test.js b/sites/magticom.ge/magticom.ge.test.js index 9282ebde3..9b3c82cdc 100644 --- a/sites/magticom.ge/magticom.ge.test.js +++ b/sites/magticom.ge/magticom.ge.test.js @@ -1,66 +1,66 @@ -// npm run channels:parse -- --config=./sites/magticom.ge/magticom.ge.config.js --output=./sites/magticom.ge/magticom.ge.channels.xml -// npm run grab -- --site=magticom.ge - -const { parser, url, request } = require('./magticom.ge.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '260', - xmltv_id: 'BollywoodHDRussia.ru' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.magticom.ge/request/channel-program.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.has('channelId')).toBe(true) - expect(result.has('start')).toBe(true) - expect(result.has('end')).toBe(true) -}) - -it('can parse response', () => { - const content = - '[{"id":2313254118,"channelId":260,"startTimestamp":"2021-11-22T07:00:00","endTimestamp":"2021-11-22T09:00:00","duration":null,"title":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","subTitle":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","info":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434.","pg":null,"year":null,"country":null,"imageUrl":null,"createdBy":-200,"creationTimestamp":"2021-11-21T18:04:52","epgSourceId":8,"startDateStr":"20211122070000","genreByGenreId":null,"languageByLanguageId":{"id":3,"name":"\\u10e0\\u10e3\\u10e1\\u10e3\\u10da\\u10d8","orderIndex":3,"nameShort":"ru"},"externalId":"2021460000084132","programHumanById":[],"date":null,"time":null,"startDate":null,"endDate":null,"longInfo":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434."}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-22T03:00:00.000Z', - stop: '2021-11-22T05:00:00.000Z', - title: 'Х/ф "Неравный брак".', - description: - 'Гуджаратец Хасмукх Пател поссорился с новым соседом Гугги Тандоном. Но им приходится помириться, когда их дети влюбляются друг в друга. Режиссер: Санджай Чхел. Актеры: Риши Капур, Пареш Равал, Вир Дас. 2017 год.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/magticom.ge/magticom.ge.config.js --output=./sites/magticom.ge/magticom.ge.channels.xml +// npm run grab -- --site=magticom.ge + +const { parser, url, request } = require('./magticom.ge.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '260', + xmltv_id: 'BollywoodHDRussia.ru' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.magticom.ge/request/channel-program.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.has('channelId')).toBe(true) + expect(result.has('start')).toBe(true) + expect(result.has('end')).toBe(true) +}) + +it('can parse response', () => { + const content = + '[{"id":2313254118,"channelId":260,"startTimestamp":"2021-11-22T07:00:00","endTimestamp":"2021-11-22T09:00:00","duration":null,"title":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","subTitle":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","info":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434.","pg":null,"year":null,"country":null,"imageUrl":null,"createdBy":-200,"creationTimestamp":"2021-11-21T18:04:52","epgSourceId":8,"startDateStr":"20211122070000","genreByGenreId":null,"languageByLanguageId":{"id":3,"name":"\\u10e0\\u10e3\\u10e1\\u10e3\\u10da\\u10d8","orderIndex":3,"nameShort":"ru"},"externalId":"2021460000084132","programHumanById":[],"date":null,"time":null,"startDate":null,"endDate":null,"longInfo":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434."}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-22T03:00:00.000Z', + stop: '2021-11-22T05:00:00.000Z', + title: 'Х/ф "Неравный брак".', + description: + 'Гуджаратец Хасмукх Пател поссорился с новым соседом Гугги Тандоном. Но им приходится помириться, когда их дети влюбляются друг в друга. Режиссер: Санджай Чхел. Актеры: Риши Капур, Пареш Равал, Вир Дас. 2017 год.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mako.co.il/mako.co.il.config.js b/sites/mako.co.il/mako.co.il.config.js index 0466dfb64..73474d11b 100644 --- a/sites/mako.co.il/mako.co.il.config.js +++ b/sites/mako.co.il/mako.co.il.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mako.co.il', - days: 2, - url: 'https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp', - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(item.DurationMs, 'ms') - programs.push({ - title: item.ProgramName, - description: item.EventDescription, - icon: item.Picture, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - if (!item.StartTimeUTC) return null - - return dayjs(item.StartTimeUTC) -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programs)) return [] - const d = date.format('DD/MM/YYYY') - - return data.programs.filter(item => item.Date.startsWith(d)) -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mako.co.il', + days: 2, + url: 'https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp', + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(item.DurationMs, 'ms') + programs.push({ + title: item.ProgramName, + description: item.EventDescription, + icon: item.Picture, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + if (!item.StartTimeUTC) return null + + return dayjs(item.StartTimeUTC) +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programs)) return [] + const d = date.format('DD/MM/YYYY') + + return data.programs.filter(item => item.Date.startsWith(d)) +} diff --git a/sites/mako.co.il/mako.co.il.test.js b/sites/mako.co.il/mako.co.il.test.js index da82becac..5cb503dc8 100644 --- a/sites/mako.co.il/mako.co.il.test.js +++ b/sites/mako.co.il/mako.co.il.test.js @@ -1,42 +1,42 @@ -// npm run grab -- --site=mako.co.il - -const { parser, url } = require('./mako.co.il.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-07', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url).toBe('https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp') -}) - -it('can parse response', () => { - const content = - '{"programs":[{"DisplayEndTime":"06:15","MakoTVURL":"","HouseNumber":"L17165475","StartTimeUTC":1646539200000,"DurationMs":900000,"DisplayStartTime":"06:00","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"06/03/2022 06:00:00","RerunBroadcast":false,"Duration":"00:15","ProgramName":"כותרות הבוקר","Date":"06/03/2022 06:00:00","MakoProgramsURL":"","LiveBroadcast":true,"ProgramCode":134987,"Episode":"","Picture":"https://img.mako.co.il//2021/08/04/hadshot_haboker_im_niv_raskin.jpg","MakoShortName":"","hebrewDate":"6 במרץ","Season":"","day":"הערב","EventDescription":"","EnglishName":"cotrot,EP 46"},{"DisplayEndTime":"02:39","MakoTVURL":"","HouseNumber":"A168960","StartTimeUTC":1646613480000,"DurationMs":60000,"DisplayStartTime":"02:38","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"07/03/2022 02:38:00","RerunBroadcast":true,"Duration":"00:01","ProgramName":"רוקדים עם כוכבים - בר זומר","Date":"07/03/2022 02:38:00","MakoProgramsURL":"","LiveBroadcast":false,"ProgramCode":135029,"Episode":"","Picture":"https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg","MakoShortName":"","hebrewDate":"7 במרץ","Season":"","day":"מחר","EventDescription":"מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.","EnglishName":"rokdim,EP 10"}]}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-07T00:38:00.000Z', - stop: '2022-03-07T00:39:00.000Z', - title: 'רוקדים עם כוכבים - בר זומר', - description: 'מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.', - icon: 'https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mako.co.il + +const { parser, url } = require('./mako.co.il.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-07', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url).toBe('https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp') +}) + +it('can parse response', () => { + const content = + '{"programs":[{"DisplayEndTime":"06:15","MakoTVURL":"","HouseNumber":"L17165475","StartTimeUTC":1646539200000,"DurationMs":900000,"DisplayStartTime":"06:00","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"06/03/2022 06:00:00","RerunBroadcast":false,"Duration":"00:15","ProgramName":"כותרות הבוקר","Date":"06/03/2022 06:00:00","MakoProgramsURL":"","LiveBroadcast":true,"ProgramCode":134987,"Episode":"","Picture":"https://img.mako.co.il//2021/08/04/hadshot_haboker_im_niv_raskin.jpg","MakoShortName":"","hebrewDate":"6 במרץ","Season":"","day":"הערב","EventDescription":"","EnglishName":"cotrot,EP 46"},{"DisplayEndTime":"02:39","MakoTVURL":"","HouseNumber":"A168960","StartTimeUTC":1646613480000,"DurationMs":60000,"DisplayStartTime":"02:38","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"07/03/2022 02:38:00","RerunBroadcast":true,"Duration":"00:01","ProgramName":"רוקדים עם כוכבים - בר זומר","Date":"07/03/2022 02:38:00","MakoProgramsURL":"","LiveBroadcast":false,"ProgramCode":135029,"Episode":"","Picture":"https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg","MakoShortName":"","hebrewDate":"7 במרץ","Season":"","day":"מחר","EventDescription":"מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.","EnglishName":"rokdim,EP 10"}]}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-07T00:38:00.000Z', + stop: '2022-03-07T00:39:00.000Z', + title: 'רוקדים עם כוכבים - בר זומר', + description: 'מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.', + icon: 'https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.config.js b/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.config.js index c3d29d5fc..014d70126 100644 --- a/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.config.js +++ b/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'maxtv.hrvatskitelekom.hr', - days: 2, - url: 'https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/shows', - request: { - method: 'POST', - data: function ({ channel, date }) { - return { - channelList: [channel.site_id], - startDate: date.unix(), - endDate: date.add(1, 'd').unix() - } - } - }, - parser: function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (item.showId == -1) return - programs.push({ - title: item.title, - category: item.category, - start: dayjs.unix(item.startTime), - stop: dayjs.unix(item.endTime) - }) - }) - - return programs - } -} - -function parseContent(content, channel) { - const json = JSON.parse(content) - if (!Array.isArray(json.data)) return null - - return json.data.find(i => i.channelId == channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - - return data && Array.isArray(data.shows) ? data.shows : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'maxtv.hrvatskitelekom.hr', + days: 2, + url: 'https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/shows', + request: { + method: 'POST', + data: function ({ channel, date }) { + return { + channelList: [channel.site_id], + startDate: date.unix(), + endDate: date.add(1, 'd').unix() + } + } + }, + parser: function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (item.showId == -1) return + programs.push({ + title: item.title, + category: item.category, + start: dayjs.unix(item.startTime), + stop: dayjs.unix(item.endTime) + }) + }) + + return programs + } +} + +function parseContent(content, channel) { + const json = JSON.parse(content) + if (!Array.isArray(json.data)) return null + + return json.data.find(i => i.channelId == channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + + return data && Array.isArray(data.shows) ? data.shows : [] +} diff --git a/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.test.js b/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.test.js index 6388c9b70..fb9c66797 100644 --- a/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.test.js +++ b/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=maxtv.hrvatskitelekom.hr - -const { parser, url, request } = require('./maxtv.hrvatskitelekom.hr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-16', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '316', - xmltv_id: '24KitchenCroatia.hr' -} -const content = - '{"status":{"code":200,"message":"OK","authType":"Unauthenticated","ottSessionToken":null},"data":[{"channelId":"316","title":"24Kitchen","logo":"http://ottepg5.nexttv.ht.hr:33200/EPG/jsp/images/universal/film/logo/fileEntity/20161109/000200/XTV100002173/493d03f8-0f08-4932-8371-e5b57d96f17d.png","chanNumber":500,"hasCatchup":false,"ottChannel":true,"userSubscribed":false,"shows":[{"showId":"-1","title":"Nema informacija","startTime":1636952400,"endTime":1636967400,"category":"ostalo","hasReminder":false,"hasRecording":false,"hasSeriesRecording":false,"userOttPlayable":false,"userLocked":false,"isPPV":false,"buyPrice":""},{"showId":"17298142","title":"Najčudniji svjetski restorani","startTime":1636952400,"endTime":1636952700,"category":"Kulinarski","hasReminder":false,"hasRecording":false,"hasSeriesRecording":false,"userOttPlayable":false,"userLocked":false,"isPPV":false,"buyPrice":""}]}]}' - -it('can generate valid url', () => { - expect(url).toBe('https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/shows') -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - channelList: ['316'], - startDate: 1637020800, - endDate: 1637107200 - }) -}) - -it('can parse response', () => { - const result = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-15T05:00:00.000Z', - stop: '2021-11-15T05:05:00.000Z', - title: 'Najčudniji svjetski restorani', - category: 'Kulinarski' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"status":{"code":200,"message":"OK","authType":"Unauthenticated","ottSessionToken":null},"data":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=maxtv.hrvatskitelekom.hr + +const { parser, url, request } = require('./maxtv.hrvatskitelekom.hr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-16', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '316', + xmltv_id: '24KitchenCroatia.hr' +} +const content = + '{"status":{"code":200,"message":"OK","authType":"Unauthenticated","ottSessionToken":null},"data":[{"channelId":"316","title":"24Kitchen","logo":"http://ottepg5.nexttv.ht.hr:33200/EPG/jsp/images/universal/film/logo/fileEntity/20161109/000200/XTV100002173/493d03f8-0f08-4932-8371-e5b57d96f17d.png","chanNumber":500,"hasCatchup":false,"ottChannel":true,"userSubscribed":false,"shows":[{"showId":"-1","title":"Nema informacija","startTime":1636952400,"endTime":1636967400,"category":"ostalo","hasReminder":false,"hasRecording":false,"hasSeriesRecording":false,"userOttPlayable":false,"userLocked":false,"isPPV":false,"buyPrice":""},{"showId":"17298142","title":"Najčudniji svjetski restorani","startTime":1636952400,"endTime":1636952700,"category":"Kulinarski","hasReminder":false,"hasRecording":false,"hasSeriesRecording":false,"userOttPlayable":false,"userLocked":false,"isPPV":false,"buyPrice":""}]}]}' + +it('can generate valid url', () => { + expect(url).toBe('https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/shows') +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + channelList: ['316'], + startDate: 1637020800, + endDate: 1637107200 + }) +}) + +it('can parse response', () => { + const result = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-15T05:00:00.000Z', + stop: '2021-11-15T05:05:00.000Z', + title: 'Najčudniji svjetski restorani', + category: 'Kulinarski' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"status":{"code":200,"message":"OK","authType":"Unauthenticated","ottSessionToken":null},"data":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/maxtvgo.mk/maxtvgo.mk.config.js b/sites/maxtvgo.mk/maxtvgo.mk.config.js index 83991ae06..aa5df0aee 100644 --- a/sites/maxtvgo.mk/maxtvgo.mk.config.js +++ b/sites/maxtvgo.mk/maxtvgo.mk.config.js @@ -1,72 +1,72 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) - -module.exports = { - site: 'maxtvgo.mk', - days: 2, - url: function ({ channel, date }) { - return `https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/${ - channel.site_id - }/start/${date.format('YYYYMMDDHHmmss')}/stop/${date - .add(1, 'd') - .format('YYYYMMDDHHmmss')}/include_current/true/format/json` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - category: item.category, - description: parseDescription(item), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get( - 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/channel/all/application_id/deep_blue/device_configuration/2/instance_id/1/language/mk/http_proto/https/format/json' - ) - .then(r => r.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'mk', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseStart(item) { - return dayjs(item['@attributes'].start, 'YYYYMMDDHHmmss ZZ') -} - -function parseStop(item) { - return dayjs(item['@attributes'].stop, 'YYYYMMDDHHmmss ZZ') -} - -function parseDescription(item) { - return typeof item.desc === 'string' ? item.desc : null -} - -function parseIcon(item) { - return item.icon['@attributes'].src -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programme)) return [] - - return data.programme -} +const axios = require('axios') +const dayjs = require('dayjs') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) + +module.exports = { + site: 'maxtvgo.mk', + days: 2, + url: function ({ channel, date }) { + return `https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/${ + channel.site_id + }/start/${date.format('YYYYMMDDHHmmss')}/stop/${date + .add(1, 'd') + .format('YYYYMMDDHHmmss')}/include_current/true/format/json` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + category: item.category, + description: parseDescription(item), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get( + 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/channel/all/application_id/deep_blue/device_configuration/2/instance_id/1/language/mk/http_proto/https/format/json' + ) + .then(r => r.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'mk', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseStart(item) { + return dayjs(item['@attributes'].start, 'YYYYMMDDHHmmss ZZ') +} + +function parseStop(item) { + return dayjs(item['@attributes'].stop, 'YYYYMMDDHHmmss ZZ') +} + +function parseDescription(item) { + return typeof item.desc === 'string' ? item.desc : null +} + +function parseIcon(item) { + return item.icon['@attributes'].src +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programme)) return [] + + return data.programme +} diff --git a/sites/maxtvgo.mk/maxtvgo.mk.test.js b/sites/maxtvgo.mk/maxtvgo.mk.test.js index 18c7fa978..fb95a1eba 100644 --- a/sites/maxtvgo.mk/maxtvgo.mk.test.js +++ b/sites/maxtvgo.mk/maxtvgo.mk.test.js @@ -1,73 +1,73 @@ -// npm run channels:parse -- --config=./sites/maxtvgo.mk/maxtvgo.mk.config.js --output=./sites/maxtvgo.mk/maxtvgo.mk.channels.xml -// npm run grab -- --site=maxtvgo.mk - -const { parser, url } = require('./maxtvgo.mk.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '105', - xmltv_id: 'MRT1.mk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/105/start/20211117000000/stop/20211118000000/include_current/true/format/json' - ) -}) - -it('can parse response', () => { - const content = - '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":"Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.","icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T22:10:00.000Z', - stop: '2021-11-17T00:00:00.000Z', - title: 'Палмето - игран филм', - category: 'Останато', - description: - 'Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.', - icon: 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' - } - ]) -}) - -it('can parse response with no description', () => { - const content = - '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":{},"icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T22:10:00.000Z', - stop: '2021-11-17T00:00:00.000Z', - title: 'Палмето - игран филм', - category: 'Останато', - description: null, - icon: 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"@attributes":{"source-info-name":"maxtvgo.mk","generator-info-name":"spectar_epg"}}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/maxtvgo.mk/maxtvgo.mk.config.js --output=./sites/maxtvgo.mk/maxtvgo.mk.channels.xml +// npm run grab -- --site=maxtvgo.mk + +const { parser, url } = require('./maxtvgo.mk.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '105', + xmltv_id: 'MRT1.mk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/105/start/20211117000000/stop/20211118000000/include_current/true/format/json' + ) +}) + +it('can parse response', () => { + const content = + '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":"Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.","icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T22:10:00.000Z', + stop: '2021-11-17T00:00:00.000Z', + title: 'Палмето - игран филм', + category: 'Останато', + description: + 'Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.', + icon: 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' + } + ]) +}) + +it('can parse response with no description', () => { + const content = + '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":{},"icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T22:10:00.000Z', + stop: '2021-11-17T00:00:00.000Z', + title: 'Палмето - игран филм', + category: 'Останато', + description: null, + icon: 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"@attributes":{"source-info-name":"maxtvgo.mk","generator-info-name":"spectar_epg"}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mbc.net/mbc.net.config.js b/sites/mbc.net/mbc.net.config.js index b8ddf0edf..8e2b8d88c 100644 --- a/sites/mbc.net/mbc.net.config.js +++ b/sites/mbc.net/mbc.net.config.js @@ -1,40 +1,40 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'mbc.net', - days: 2, - url({ date, channel }) { - return `https://www.mbc.net/.rest/api/channel/grids?from=${date.valueOf()}&to=${date - .add(1, 'd') - .valueOf()}&channel=${channel.site_id}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.showPageTitle, - category: item.showPageGenreInArabic, - description: item.showPageAboutInArabic, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs(item.startTime).toJSON() -} - -function parseStop(item) { - return dayjs(item.endTime).toJSON() -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data -} +const dayjs = require('dayjs') + +module.exports = { + site: 'mbc.net', + days: 2, + url({ date, channel }) { + return `https://www.mbc.net/.rest/api/channel/grids?from=${date.valueOf()}&to=${date + .add(1, 'd') + .valueOf()}&channel=${channel.site_id}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.showPageTitle, + category: item.showPageGenreInArabic, + description: item.showPageAboutInArabic, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs(item.startTime).toJSON() +} + +function parseStop(item) { + return dayjs(item.endTime).toJSON() +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data +} diff --git a/sites/mbc.net/mbc.net.test.js b/sites/mbc.net/mbc.net.test.js index 554e61c01..23318c2f4 100644 --- a/sites/mbc.net/mbc.net.test.js +++ b/sites/mbc.net/mbc.net.test.js @@ -1,46 +1,46 @@ -// npm run grab -- --site=mbc.net - -const { parser, url } = require('./mbc.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'mbc1', - xmltv_id: 'MBC.ae' -} -const content = - '[{"id":3140240,"channelBCMId":"1","channelLabel":"MBC1","showPageTitle":"اختطاف","showPageGenreInArabic":" دراما","showPageAboutInArabic":".يستضيف برنامج تلفزيوني والدة لينا وشقيقتها، ولدى مشاهدتها الحلقة، تكتشف والدة ماجد الحقيقة، بينما تتعرض العنود لحادث سير مروع","startTime":1636155131000,"endTime":1636157806000,"startTimeInMilliseconds":1636155131000,"endTimeInMilliseconds":1636157806200,"premiereMode":"Fast Repeat","showingNow":false}]' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.mbc.net/.rest/api/channel/grids?from=1636156800000&to=1636243200000&channel=mbc1' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-05T23:32:11.000Z', - stop: '2021-11-06T00:16:46.000Z', - title: 'اختطاف', - category: ' دراما', - description: - '.يستضيف برنامج تلفزيوني والدة لينا وشقيقتها، ولدى مشاهدتها الحلقة، تكتشف والدة ماجد الحقيقة، بينما تتعرض العنود لحادث سير مروع' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mbc.net + +const { parser, url } = require('./mbc.net.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'mbc1', + xmltv_id: 'MBC.ae' +} +const content = + '[{"id":3140240,"channelBCMId":"1","channelLabel":"MBC1","showPageTitle":"اختطاف","showPageGenreInArabic":" دراما","showPageAboutInArabic":".يستضيف برنامج تلفزيوني والدة لينا وشقيقتها، ولدى مشاهدتها الحلقة، تكتشف والدة ماجد الحقيقة، بينما تتعرض العنود لحادث سير مروع","startTime":1636155131000,"endTime":1636157806000,"startTimeInMilliseconds":1636155131000,"endTimeInMilliseconds":1636157806200,"premiereMode":"Fast Repeat","showingNow":false}]' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.mbc.net/.rest/api/channel/grids?from=1636156800000&to=1636243200000&channel=mbc1' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-05T23:32:11.000Z', + stop: '2021-11-06T00:16:46.000Z', + title: 'اختطاف', + category: ' دراما', + description: + '.يستضيف برنامج تلفزيوني والدة لينا وشقيقتها، ولدى مشاهدتها الحلقة، تكتشف والدة ماجد الحقيقة، بينما تتعرض العنود لحادث سير مروع' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mediagenie.co.kr/mediagenie.co.kr.config.js b/sites/mediagenie.co.kr/mediagenie.co.kr.config.js index 32f85cbef..35fcb0078 100644 --- a/sites/mediagenie.co.kr/mediagenie.co.kr.config.js +++ b/sites/mediagenie.co.kr/mediagenie.co.kr.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mediagenie.co.kr', - days: 1, - url({ channel, date }) { - return `https://mediagenie.co.kr/${channel.site_id}/?qd=${date.format('YYYYMMDD')}` - }, - request: { - headers: { - cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.col2').clone().children().remove().end().text().trim() -} - -function parseRating($item) { - const rating = $item('.col6').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseStart($item, date) { - const time = $item('.col1').text().trim() - - if (!/^\d{2}:\d{2}$/.test(time)) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.tbl > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mediagenie.co.kr', + days: 1, + url({ channel, date }) { + return `https://mediagenie.co.kr/${channel.site_id}/?qd=${date.format('YYYYMMDD')}` + }, + request: { + headers: { + cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.col2').clone().children().remove().end().text().trim() +} + +function parseRating($item) { + const rating = $item('.col6').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseStart($item, date) { + const time = $item('.col1').text().trim() + + if (!/^\d{2}:\d{2}$/.test(time)) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.tbl > tbody > tr').toArray() +} diff --git a/sites/mediagenie.co.kr/mediagenie.co.kr.test.js b/sites/mediagenie.co.kr/mediagenie.co.kr.test.js index a50aa036c..75004f2b0 100644 --- a/sites/mediagenie.co.kr/mediagenie.co.kr.test.js +++ b/sites/mediagenie.co.kr/mediagenie.co.kr.test.js @@ -1,65 +1,65 @@ -// npm run grab -- --site=mediagenie.co.kr - -const { parser, url, request } = require('./mediagenie.co.kr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ENA_DRAMA', - xmltv_id: 'ENADRAMA.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://mediagenie.co.kr/ENA_DRAMA/?qd=20230125') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-24T15:20:00.000Z', - stop: '2023-01-24T16:34:00.000Z', - title: '대행사', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[16]).toMatchObject({ - start: '2023-01-25T14:27:00.000Z', - stop: '2023-01-25T14:57:00.000Z', - title: '법쩐', - rating: { - system: 'KMRB', - value: '15' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=mediagenie.co.kr + +const { parser, url, request } = require('./mediagenie.co.kr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ENA_DRAMA', + xmltv_id: 'ENADRAMA.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://mediagenie.co.kr/ENA_DRAMA/?qd=20230125') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-24T15:20:00.000Z', + stop: '2023-01-24T16:34:00.000Z', + title: '대행사', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[16]).toMatchObject({ + start: '2023-01-25T14:27:00.000Z', + stop: '2023-01-25T14:57:00.000Z', + title: '법쩐', + rating: { + system: 'KMRB', + value: '15' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/mediaklikk.hu/mediaklikk.hu.config.js b/sites/mediaklikk.hu/mediaklikk.hu.config.js index c525911a6..26f41c641 100644 --- a/sites/mediaklikk.hu/mediaklikk.hu.config.js +++ b/sites/mediaklikk.hu/mediaklikk.hu.config.js @@ -1,85 +1,85 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mediaklikk.hu', - days: 2, - url: 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: function ({ date, channel }) { - const params = new URLSearchParams() - params.append('ChannelIds', `${channel.site_id},`) - params.append('Date', date.format('YYYY-MM-DD')) - - return params - } - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item) - let stop = parseStop($item) - if (!stop) stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item) { - const timeString = $item('*').data('from') - - return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') -} - -function parseStop($item) { - const timeString = $item('*').data('till') - if (!timeString || /^\+/.test(timeString)) return null - - try { - return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') - } catch (err) { - return null - } -} - -function parseTitle($item) { - return $item('.program_info > h1').text().trim() -} - -function parseDescription($item) { - return $item('.program_about > .program_description > p').text().trim() -} - -function parseIcon($item) { - const backgroundImage = $item('.program_about > .program_photo').css('background-image') - if (!backgroundImage) return null - const [, icon] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] - if (!icon) return null - - return `https:${icon}` -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('li.program_body').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mediaklikk.hu', + days: 2, + url: 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: function ({ date, channel }) { + const params = new URLSearchParams() + params.append('ChannelIds', `${channel.site_id},`) + params.append('Date', date.format('YYYY-MM-DD')) + + return params + } + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item) + let stop = parseStop($item) + if (!stop) stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item) { + const timeString = $item('*').data('from') + + return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') +} + +function parseStop($item) { + const timeString = $item('*').data('till') + if (!timeString || /^\+/.test(timeString)) return null + + try { + return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') + } catch (err) { + return null + } +} + +function parseTitle($item) { + return $item('.program_info > h1').text().trim() +} + +function parseDescription($item) { + return $item('.program_about > .program_description > p').text().trim() +} + +function parseIcon($item) { + const backgroundImage = $item('.program_about > .program_photo').css('background-image') + if (!backgroundImage) return null + const [, icon] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] + if (!icon) return null + + return `https:${icon}` +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('li.program_body').toArray() +} diff --git a/sites/mediaklikk.hu/mediaklikk.hu.test.js b/sites/mediaklikk.hu/mediaklikk.hu.test.js index 08b76e2cf..b6e66b7e0 100644 --- a/sites/mediaklikk.hu/mediaklikk.hu.test.js +++ b/sites/mediaklikk.hu/mediaklikk.hu.test.js @@ -1,73 +1,73 @@ -// npm run grab -- --site=mediaklikk.hu - -const { parser, url, request } = require('./mediaklikk.hu.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3', - xmltv_id: 'DuneTV.hu' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date, channel }) - expect(result.get('ChannelIds')).toBe('3,') - expect(result.get('Date')).toBe('2022-03-10') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-27T22:00:46.000Z', - stop: '2022-10-27T22:54:00.000Z', - title: 'A hegyi doktor - I. évad', - description: - 'Maxl iskolatársának, Vroninak az anyja egy autóbalesetben meghal. A 20 éves testvér, Vinzenz magához szeretné venni a lányt, ám a gyámüggyel problémái akadnak, ezért megpróbálja elszöktetni.(Eredeti hang digitálisan.)', - icon: 'https://mediaklikk.hu/wp-content/uploads/sites/4/2019/10/A-hegyi-doktor-I-évad-e1571318391226-150x150.jpg' - }) - - expect(results[56]).toMatchObject({ - start: '2022-10-28T20:35:05.000Z', - stop: '2022-10-28T21:05:05.000Z', - title: 'Szemtől szemben (1967)', - description: - 'Brad Fletcher bostoni történelemtanár, aki a délnyugati határvidéken kúrálja tüdőbetegségét, egy véletlen folytán összeakad Beauregard Bennett körözött útonállóval, akit végül maga segít a menekülésben. A tanárt lenyűgözi a törvényen kívüliek világa és felismeri, hogy értelmi felsőbbrendűségével bámulatosan tudja irányítani az embereket. Bennett csakhamar azt veszi észre, hogy a peremre szorult saját bandájában. Eközben a Pinkerton ügynökség beépített embere is csapdába igyekszik csalni mindnyájukat.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mediaklikk.hu + +const { parser, url, request } = require('./mediaklikk.hu.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3', + xmltv_id: 'DuneTV.hu' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date, channel }) + expect(result.get('ChannelIds')).toBe('3,') + expect(result.get('Date')).toBe('2022-03-10') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-27T22:00:46.000Z', + stop: '2022-10-27T22:54:00.000Z', + title: 'A hegyi doktor - I. évad', + description: + 'Maxl iskolatársának, Vroninak az anyja egy autóbalesetben meghal. A 20 éves testvér, Vinzenz magához szeretné venni a lányt, ám a gyámüggyel problémái akadnak, ezért megpróbálja elszöktetni.(Eredeti hang digitálisan.)', + icon: 'https://mediaklikk.hu/wp-content/uploads/sites/4/2019/10/A-hegyi-doktor-I-évad-e1571318391226-150x150.jpg' + }) + + expect(results[56]).toMatchObject({ + start: '2022-10-28T20:35:05.000Z', + stop: '2022-10-28T21:05:05.000Z', + title: 'Szemtől szemben (1967)', + description: + 'Brad Fletcher bostoni történelemtanár, aki a délnyugati határvidéken kúrálja tüdőbetegségét, egy véletlen folytán összeakad Beauregard Bennett körözött útonállóval, akit végül maga segít a menekülésben. A tanárt lenyűgözi a törvényen kívüliek világa és felismeri, hogy értelmi felsőbbrendűségével bámulatosan tudja irányítani az embereket. Bennett csakhamar azt veszi észre, hogy a peremre szorult saját bandájában. Eközben a Pinkerton ügynökség beépített embere is csapdába igyekszik csalni mindnyájukat.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mediaset.it/mediaset.it.config.js b/sites/mediaset.it/mediaset.it.config.js index 40a848762..a5c0a82c6 100644 --- a/sites/mediaset.it/mediaset.it.config.js +++ b/sites/mediaset.it/mediaset.it.config.js @@ -1,49 +1,49 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mediaset.it', - days: 2, - url: function ({ date, channel }) { - return `http://www.mediaset.it/guidatv/inc/canali/${date.format('YYYYMM')}/${date.format( - 'YYYYMMDD' - )}_${channel.site_id}.sjson` - }, - parser: function ({ content, date }) { - const programs = [] - const data = JSON.parse(content) - if (!data.events) return programs - - data.events.forEach(item => { - if (item.title && item.startTime && item.endTime) { - const start = dayjs - .utc(item.startTime, 'HH:mm') - .set('D', date.get('D')) - .set('M', date.get('M')) - .set('y', date.get('y')) - .toString() - - const stop = dayjs - .utc(item.endTime, 'HH:mm') - .set('D', date.get('D')) - .set('M', date.get('M')) - .set('y', date.get('y')) - .toString() - - programs.push({ - title: item.displayTitle || item.title, - description: item.description, - category: item.genere, - start, - stop - }) - } - }) - - return programs - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mediaset.it', + days: 2, + url: function ({ date, channel }) { + return `http://www.mediaset.it/guidatv/inc/canali/${date.format('YYYYMM')}/${date.format( + 'YYYYMMDD' + )}_${channel.site_id}.sjson` + }, + parser: function ({ content, date }) { + const programs = [] + const data = JSON.parse(content) + if (!data.events) return programs + + data.events.forEach(item => { + if (item.title && item.startTime && item.endTime) { + const start = dayjs + .utc(item.startTime, 'HH:mm') + .set('D', date.get('D')) + .set('M', date.get('M')) + .set('y', date.get('y')) + .toString() + + const stop = dayjs + .utc(item.endTime, 'HH:mm') + .set('D', date.get('D')) + .set('M', date.get('M')) + .set('y', date.get('y')) + .toString() + + programs.push({ + title: item.displayTitle || item.title, + description: item.description, + category: item.genere, + start, + stop + }) + } + }) + + return programs + } +} diff --git a/sites/melita.com/melita.com.config.js b/sites/melita.com/melita.com.config.js index 7fff861ae..0bd89831c 100644 --- a/sites/melita.com/melita.com.config.js +++ b/sites/melita.com/melita.com.config.js @@ -1,88 +1,88 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'melita.com', - days: 2, - url: function ({ channel, date }) { - return `https://androme.melitacable.com/api/epg/v1/schedule/channel/${ - channel.site_id - }/from/${date.format('YYYY-MM-DDTHH:mmZ')}/until/${date - .add(1, 'd') - .format('YYYY-MM-DDTHH:mmZ')}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.shortSynopsis, - icon: parseIcon(item), - category: item.tags, - season: item.season, - episode: item.episode, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://androme.melitacable.com/api/epg/v2/channel') - .then(r => r.data) - .catch(console.log) - - return channels - .filter(i => !i.audioOnly && i.enabled) - .map(i => { - return { - name: i.name, - site_id: i.id - } - }) - } -} - -function parseStart(item) { - if (!item.published || !item.published.start) return null - - return dayjs(item.published.start) -} - -function parseStop(item) { - if (!item.published || !item.published.end) return null - - return dayjs(item.published.end) -} - -function parseIcon(item) { - return item.posterImage ? item.posterImage + '?form=epg-card-6' : null -} - -function parseItems(content) { - const data = JSON.parse(content) - if ( - !data || - !data.schedules || - !data.programs || - !data.seasons || - !data.series || - !Array.isArray(data.schedules) - ) - return [] - - return data.schedules - .map(i => { - const program = data.programs.find(p => p.id === i.program) || {} - if (!program.season) return null - const season = data.seasons.find(s => s.id === program.season) || {} - if (!season.series) return null - const series = data.series.find(s => s.id === season.series) - - return { ...i, ...program, ...season, ...series } - }) - .filter(i => i) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'melita.com', + days: 2, + url: function ({ channel, date }) { + return `https://androme.melitacable.com/api/epg/v1/schedule/channel/${ + channel.site_id + }/from/${date.format('YYYY-MM-DDTHH:mmZ')}/until/${date + .add(1, 'd') + .format('YYYY-MM-DDTHH:mmZ')}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.shortSynopsis, + icon: parseIcon(item), + category: item.tags, + season: item.season, + episode: item.episode, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://androme.melitacable.com/api/epg/v2/channel') + .then(r => r.data) + .catch(console.log) + + return channels + .filter(i => !i.audioOnly && i.enabled) + .map(i => { + return { + name: i.name, + site_id: i.id + } + }) + } +} + +function parseStart(item) { + if (!item.published || !item.published.start) return null + + return dayjs(item.published.start) +} + +function parseStop(item) { + if (!item.published || !item.published.end) return null + + return dayjs(item.published.end) +} + +function parseIcon(item) { + return item.posterImage ? item.posterImage + '?form=epg-card-6' : null +} + +function parseItems(content) { + const data = JSON.parse(content) + if ( + !data || + !data.schedules || + !data.programs || + !data.seasons || + !data.series || + !Array.isArray(data.schedules) + ) + return [] + + return data.schedules + .map(i => { + const program = data.programs.find(p => p.id === i.program) || {} + if (!program.season) return null + const season = data.seasons.find(s => s.id === program.season) || {} + if (!season.series) return null + const series = data.series.find(s => s.id === season.series) + + return { ...i, ...program, ...season, ...series } + }) + .filter(i => i) +} diff --git a/sites/melita.com/melita.com.test.js b/sites/melita.com/melita.com.test.js index 3fa48f759..d126668a2 100644 --- a/sites/melita.com/melita.com.test.js +++ b/sites/melita.com/melita.com.test.js @@ -1,52 +1,52 @@ -// npm run channels:parse -- --config=./sites/melita.com/melita.com.config.js --output=./sites/melita.com/melita.com.channels.xml -// npm run grab -- --site=melita.com - -const { parser, url } = require('./melita.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-04-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '4d40a9f9-12fd-4f03-8072-61c637ff6995', - xmltv_id: 'TVM.mt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://androme.melitacable.com/api/epg/v1/schedule/channel/4d40a9f9-12fd-4f03-8072-61c637ff6995/from/2022-04-20T00:00+00:00/until/2022-04-21T00:00+00:00' - ) -}) - -it('can parse response', () => { - const content = - '{"schedules":[{"id":"138dabff-131a-42a0-9373-203545933dd0","published":{"start":"2022-04-20T06:25:00Z","end":"2022-04-20T06:45:00Z"},"program":"ae52299a-3c99-4d34-9932-e21d383f9800","live":false,"blackouts":[]}],"programs":[{"id":"ae52299a-3c99-4d34-9932-e21d383f9800","title":"How I Met Your Mother","shortSynopsis":"Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.","posterImage":"https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg","episode":12,"episodeTitle":"Symphony of Illumination","season":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","genres":["3.5.7.3"],"tags":["comedy"],"adult":false}],"seasons":[{"id":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","title":"How I Met Your Mother","adult":false,"season":7,"series":"858c535a-abbb-451b-807a-94196997ea2d"}],"series":[{"id":"858c535a-abbb-451b-807a-94196997ea2d","title":"How I Met Your Mother","adult":false}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-04-20T06:25:00.000Z', - stop: '2022-04-20T06:45:00.000Z', - title: 'How I Met Your Mother', - description: - 'Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.', - season: 7, - episode: 12, - icon: 'https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg?form=epg-card-6', - category: ['comedy'] - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/melita.com/melita.com.config.js --output=./sites/melita.com/melita.com.channels.xml +// npm run grab -- --site=melita.com + +const { parser, url } = require('./melita.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-04-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '4d40a9f9-12fd-4f03-8072-61c637ff6995', + xmltv_id: 'TVM.mt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://androme.melitacable.com/api/epg/v1/schedule/channel/4d40a9f9-12fd-4f03-8072-61c637ff6995/from/2022-04-20T00:00+00:00/until/2022-04-21T00:00+00:00' + ) +}) + +it('can parse response', () => { + const content = + '{"schedules":[{"id":"138dabff-131a-42a0-9373-203545933dd0","published":{"start":"2022-04-20T06:25:00Z","end":"2022-04-20T06:45:00Z"},"program":"ae52299a-3c99-4d34-9932-e21d383f9800","live":false,"blackouts":[]}],"programs":[{"id":"ae52299a-3c99-4d34-9932-e21d383f9800","title":"How I Met Your Mother","shortSynopsis":"Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.","posterImage":"https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg","episode":12,"episodeTitle":"Symphony of Illumination","season":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","genres":["3.5.7.3"],"tags":["comedy"],"adult":false}],"seasons":[{"id":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","title":"How I Met Your Mother","adult":false,"season":7,"series":"858c535a-abbb-451b-807a-94196997ea2d"}],"series":[{"id":"858c535a-abbb-451b-807a-94196997ea2d","title":"How I Met Your Mother","adult":false}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-04-20T06:25:00.000Z', + stop: '2022-04-20T06:45:00.000Z', + title: 'How I Met Your Mother', + description: + 'Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.', + season: 7, + episode: 12, + icon: 'https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg?form=epg-card-6', + category: ['comedy'] + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/meo.pt/meo.pt.config.js b/sites/meo.pt/meo.pt.config.js index baa87c55e..1e95c4ee7 100644 --- a/sites/meo.pt/meo.pt.config.js +++ b/sites/meo.pt/meo.pt.config.js @@ -1,60 +1,60 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'meo.pt', - days: 2, - url: 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels', - request: { - method: 'POST', - headers: { - Origin: 'https://www.meo.pt' - }, - data: function ({ channel, date }) { - return { - service: 'channelsguide', - channels: [channel.site_id], - dateStart: date.format('YYYY-MM-DDT00:00:00-00:00'), - dateEnd: date.add(1, 'd').format('YYYY-MM-DDT00:00:00-00:00'), - accountID: '' - } - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - let stop = parseStop(item) - if (stop < start) { - stop = stop.plus({ days: 1 }) - } - programs.push({ - title: item.name, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - return DateTime.fromFormat(`${item.date} ${item.timeIni}`, 'd-M-yyyy HH:mm', { - zone: 'Europe/Lisbon' - }).toUTC() -} - -function parseStop(item) { - return DateTime.fromFormat(`${item.date} ${item.timeEnd}`, 'd-M-yyyy HH:mm', { - zone: 'Europe/Lisbon' - }).toUTC() -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - const programs = data?.d?.channels?.[0]?.programs - - return Array.isArray(programs) ? programs : [] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'meo.pt', + days: 2, + url: 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels', + request: { + method: 'POST', + headers: { + Origin: 'https://www.meo.pt' + }, + data: function ({ channel, date }) { + return { + service: 'channelsguide', + channels: [channel.site_id], + dateStart: date.format('YYYY-MM-DDT00:00:00-00:00'), + dateEnd: date.add(1, 'd').format('YYYY-MM-DDT00:00:00-00:00'), + accountID: '' + } + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + let stop = parseStop(item) + if (stop < start) { + stop = stop.plus({ days: 1 }) + } + programs.push({ + title: item.name, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + return DateTime.fromFormat(`${item.date} ${item.timeIni}`, 'd-M-yyyy HH:mm', { + zone: 'Europe/Lisbon' + }).toUTC() +} + +function parseStop(item) { + return DateTime.fromFormat(`${item.date} ${item.timeEnd}`, 'd-M-yyyy HH:mm', { + zone: 'Europe/Lisbon' + }).toUTC() +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + const programs = data?.d?.channels?.[0]?.programs + + return Array.isArray(programs) ? programs : [] +} diff --git a/sites/meo.pt/meo.pt.test.js b/sites/meo.pt/meo.pt.test.js index e4fe3ccb0..7fb4019bf 100644 --- a/sites/meo.pt/meo.pt.test.js +++ b/sites/meo.pt/meo.pt.test.js @@ -1,62 +1,62 @@ -// npm run grab -- --site=meo.pt - -const { parser, url, request } = require('./meo.pt.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'RTPM', - xmltv_id: 'RTPMadeira.pt' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Origin: 'https://www.meo.pt' - }) -}) - -it('can generate valid request method', () => { - expect(request.data({ channel, date })).toMatchObject({ - service: 'channelsguide', - channels: ['RTPM'], - dateStart: '2022-12-02T00:00:00-00:00', - dateEnd: '2022-12-03T00:00:00-00:00', - accountID: '' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T23:35:00.000Z', - stop: '2022-12-02T00:17:00.000Z', - title: 'Walker, O Ranger Do Texas T6 - Ep. 14' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=meo.pt + +const { parser, url, request } = require('./meo.pt.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'RTPM', + xmltv_id: 'RTPMadeira.pt' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Origin: 'https://www.meo.pt' + }) +}) + +it('can generate valid request method', () => { + expect(request.data({ channel, date })).toMatchObject({ + service: 'channelsguide', + channels: ['RTPM'], + dateStart: '2022-12-02T00:00:00-00:00', + dateEnd: '2022-12-03T00:00:00-00:00', + accountID: '' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T23:35:00.000Z', + stop: '2022-12-02T00:17:00.000Z', + title: 'Walker, O Ranger Do Texas T6 - Ep. 14' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mewatch.sg/mewatch.sg.config.js b/sites/mewatch.sg/mewatch.sg.config.js index baf58b9ad..bb0290501 100644 --- a/sites/mewatch.sg/mewatch.sg.config.js +++ b/sites/mewatch.sg/mewatch.sg.config.js @@ -1,61 +1,61 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'mewatch.sg', - days: 2, - url: function ({ channel, date }) { - return `https://cdn.mewatch.sg/api/schedules?channels=${channel.site_id}&date=${date.format( - 'YYYY-MM-DD' - )}&duration=24&ff=idp,ldp,rpt,cd&hour=21&intersect=true&lang=en&segments=all` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const info = item.item - programs.push({ - title: info.title, - description: info.description, - icon: info.images.tile, - episode: info.episodeNumber, - season: info.seasonNumber, - start: parseStart(item), - stop: parseStop(item), - rating: parseRating(info) - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs(item.startDate) -} - -function parseStop(item) { - return dayjs(item.endDate) -} - -function parseRating(info) { - const classification = info.classification - if (classification && classification.code) { - const [, system, value] = classification.code.match(/^([A-Z]+)-([A-Z0-9]+)/) || [ - null, - null, - null - ] - - return { system, value } - } - - return null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data)) return [] - const channelData = data.find(i => i.channelId === channel.site_id) - - return channelData && Array.isArray(channelData.schedules) ? channelData.schedules : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'mewatch.sg', + days: 2, + url: function ({ channel, date }) { + return `https://cdn.mewatch.sg/api/schedules?channels=${channel.site_id}&date=${date.format( + 'YYYY-MM-DD' + )}&duration=24&ff=idp,ldp,rpt,cd&hour=21&intersect=true&lang=en&segments=all` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const info = item.item + programs.push({ + title: info.title, + description: info.description, + icon: info.images.tile, + episode: info.episodeNumber, + season: info.seasonNumber, + start: parseStart(item), + stop: parseStop(item), + rating: parseRating(info) + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs(item.startDate) +} + +function parseStop(item) { + return dayjs(item.endDate) +} + +function parseRating(info) { + const classification = info.classification + if (classification && classification.code) { + const [, system, value] = classification.code.match(/^([A-Z]+)-([A-Z0-9]+)/) || [ + null, + null, + null + ] + + return { system, value } + } + + return null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data)) return [] + const channelData = data.find(i => i.channelId === channel.site_id) + + return channelData && Array.isArray(channelData.schedules) ? channelData.schedules : [] +} diff --git a/sites/mewatch.sg/mewatch.sg.test.js b/sites/mewatch.sg/mewatch.sg.test.js index ca944c90b..fdc6f9c3d 100644 --- a/sites/mewatch.sg/mewatch.sg.test.js +++ b/sites/mewatch.sg/mewatch.sg.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=mewatch.sg - -const { parser, url } = require('./mewatch.sg.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-06-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '97098', - xmltv_id: 'Channel5Singapore.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://cdn.mewatch.sg/api/schedules?channels=97098&date=2022-06-11&duration=24&ff=idp,ldp,rpt,cd&hour=21&intersect=true&lang=en&segments=all' - ) -}) - -it('can parse response', () => { - const content = - '[{"channelId":"97098","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[{"channelId":"97098","customId":"37040748","endDate":"2022-06-11T21:30:00Z","id":"788a7dd","live":false,"startDate":"2022-06-11T21:00:00Z","isGap":false,"InteractiveType":"0","item":{"type":"episode","title":"Open Homes S3 - EP 2","blackoutMessage":"Programme is not available for live streaming.","description":"Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.","classification":{"code":"IMDA-G (Violence)","name":"G (Violence)"},"episodeNumber":2,"episodeTitle":"Collaroy, Sydney","seasonNumber":3,"images":{"wallpaper":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853691\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all","tile":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853697\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all"},"enableCatchUp":true,"enableStartOver":false,"enableSeeking":false,"programSource":"ACQUIRED","simulcast":"LOCAL","masterReferenceKey":"0CH50CH5A0105567800020A0000000000P3254400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}]}]' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-06-11T21:00:00.000Z', - stop: '2022-06-11T21:30:00.000Z', - title: 'Open Homes S3 - EP 2', - description: - 'Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.', - icon: "https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all", - episode: 2, - season: 3, - rating: { - system: 'IMDA', - value: 'G' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '[{"channelId":"9798","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[]}]', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mewatch.sg + +const { parser, url } = require('./mewatch.sg.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-06-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '97098', + xmltv_id: 'Channel5Singapore.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://cdn.mewatch.sg/api/schedules?channels=97098&date=2022-06-11&duration=24&ff=idp,ldp,rpt,cd&hour=21&intersect=true&lang=en&segments=all' + ) +}) + +it('can parse response', () => { + const content = + '[{"channelId":"97098","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[{"channelId":"97098","customId":"37040748","endDate":"2022-06-11T21:30:00Z","id":"788a7dd","live":false,"startDate":"2022-06-11T21:00:00Z","isGap":false,"InteractiveType":"0","item":{"type":"episode","title":"Open Homes S3 - EP 2","blackoutMessage":"Programme is not available for live streaming.","description":"Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.","classification":{"code":"IMDA-G (Violence)","name":"G (Violence)"},"episodeNumber":2,"episodeTitle":"Collaroy, Sydney","seasonNumber":3,"images":{"wallpaper":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853691\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all","tile":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853697\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all"},"enableCatchUp":true,"enableStartOver":false,"enableSeeking":false,"programSource":"ACQUIRED","simulcast":"LOCAL","masterReferenceKey":"0CH50CH5A0105567800020A0000000000P3254400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}]}]' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-06-11T21:00:00.000Z', + stop: '2022-06-11T21:30:00.000Z', + title: 'Open Homes S3 - EP 2', + description: + 'Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.', + icon: "https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all", + episode: 2, + season: 3, + rating: { + system: 'IMDA', + value: 'G' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '[{"channelId":"9798","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[]}]', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mi.tv/mi.tv.config.js b/sites/mi.tv/mi.tv.config.js index 946d10e19..1c46f095e 100644 --- a/sites/mi.tv/mi.tv.config.js +++ b/sites/mi.tv/mi.tv.config.js @@ -1,78 +1,78 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mi.tv', - days: 2, - url({ date, channel }) { - const [country, id] = channel.site_id.split('#') - - return `https://mi.tv/${country}/async/channel/${id}/${date.format('YYYY-MM-DD')}/0` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('a > div.content > span.time').text() - if (!timeString) return null - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return dayjs.utc(dateString, 'MM/DD/YYYY HH:mm') -} - -function parseTitle($item) { - return $item('a > div.content > h2').text().trim() -} - -function parseCategory($item) { - return $item('a > div.content > span.sub-title').text().trim() -} - -function parseDescription($item) { - return $item('a > div.content > p.synopsis').text().trim() -} - -function parseIcon($item) { - const backgroundImage = $item('a > div.image-parent > div.image').css('background-image') - const [, icon] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] - - return icon -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#listings > ul > li').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mi.tv', + days: 2, + url({ date, channel }) { + const [country, id] = channel.site_id.split('#') + + return `https://mi.tv/${country}/async/channel/${id}/${date.format('YYYY-MM-DD')}/0` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('a > div.content > span.time').text() + if (!timeString) return null + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return dayjs.utc(dateString, 'MM/DD/YYYY HH:mm') +} + +function parseTitle($item) { + return $item('a > div.content > h2').text().trim() +} + +function parseCategory($item) { + return $item('a > div.content > span.sub-title').text().trim() +} + +function parseDescription($item) { + return $item('a > div.content > p.synopsis').text().trim() +} + +function parseIcon($item) { + const backgroundImage = $item('a > div.image-parent > div.image').css('background-image') + const [, icon] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] + + return icon +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#listings > ul > li').toArray() +} diff --git a/sites/mi.tv/mi.tv.test.js b/sites/mi.tv/mi.tv.test.js index 5a4a1be00..079ef682d 100644 --- a/sites/mi.tv/mi.tv.test.js +++ b/sites/mi.tv/mi.tv.test.js @@ -1,68 +1,68 @@ -// npm run grab -- --site=mi.tv - -const { parser, url } = require('./mi.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ar#24-7-canal-de-noticias', - xmltv_id: '247CanaldeNoticias.ar' -} -const content = - '' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0' - ) -}) - -it('can parse response', () => { - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T03:00:00.000Z', - stop: '2021-11-24T23:00:00.000Z', - title: 'Trasnoche de 24/7', - category: 'Interés general', - description: 'Lo más visto de la semana en nuestra pantalla.', - icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - }, - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-25T01:00:00.000Z', - title: 'Noticiero central - Segunda edición', - category: 'Noticiero', - description: - 'Cerramos el día con un completo resumen de los temas más relevantes con columnistas y análisis especiales para terminar el día.', - icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - }, - { - start: '2021-11-25T01:00:00.000Z', - stop: '2021-11-25T02:00:00.000Z', - title: 'Plus energético', - category: 'Cultural', - description: - 'La energía tiene mucho para mostrar. Este programa reúne a las principales empresas y protagonistas de la actividad que esta revolucionando la región.', - icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mi.tv + +const { parser, url } = require('./mi.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ar#24-7-canal-de-noticias', + xmltv_id: '247CanaldeNoticias.ar' +} +const content = + '' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0' + ) +}) + +it('can parse response', () => { + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T03:00:00.000Z', + stop: '2021-11-24T23:00:00.000Z', + title: 'Trasnoche de 24/7', + category: 'Interés general', + description: 'Lo más visto de la semana en nuestra pantalla.', + icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + }, + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-25T01:00:00.000Z', + title: 'Noticiero central - Segunda edición', + category: 'Noticiero', + description: + 'Cerramos el día con un completo resumen de los temas más relevantes con columnistas y análisis especiales para terminar el día.', + icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + }, + { + start: '2021-11-25T01:00:00.000Z', + stop: '2021-11-25T02:00:00.000Z', + title: 'Plus energético', + category: 'Cultural', + description: + 'La energía tiene mucho para mostrar. Este programa reúne a las principales empresas y protagonistas de la actividad que esta revolucionando la región.', + icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mncvision.id/mncvision.id.config.js b/sites/mncvision.id/mncvision.id.config.js index 1b5949f44..25e1e90fa 100644 --- a/sites/mncvision.id/mncvision.id.config.js +++ b/sites/mncvision.id/mncvision.id.config.js @@ -1,184 +1,184 @@ -const _ = require('lodash') -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mncvision.id', - days: 2, - url: 'https://mncvision.id/schedule/table', - request: { - method: 'POST', - data: function ({ channel, date }) { - const formData = new URLSearchParams() - formData.append('search_model', 'channel') - formData.append('af0rmelement', 'aformelement') - formData.append('fdate', date.format('YYYY-MM-DD')) - formData.append('fchannel', channel.site_id) - formData.append('submit', 'Search') - - return formData - }, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - jar: null - }, - async parser({ content, date, headers, channel }) { - const programs = [] - const cookies = parseCookies(headers) - if (!cookies) return programs - let items = parseItems(content) - if (!items.length) return programs - - const pages = parsePages(content) - for (let url of pages) { - items = items.concat(parseItems(await loadNextPage(url, cookies))) - } - - const langCookies = await loadLangCookies(channel) - if (!langCookies) return programs - - for (const item of items) { - const $item = cheerio.load(item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - const description = await loadDescription($item, langCookies) - programs.push({ - title: parseTitle($item), - season: parseSeason($item), - episode: parseEpisode($item), - description, - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://www.mncvision.id/schedule') - .then(response => response.data) - .catch(console.error) - - const $ = cheerio.load(data) - const items = $('select[name="fchannel"] option').toArray() - const channels = items.map(item => { - const $item = cheerio.load(item) - - return { - lang: 'id', - site_id: $item('*').attr('value'), - name: $item('*').text() - } - }) - - return channels - } -} - -function parseSeason($item) { - const title = parseTitle($item) - const [, season] = title.match(/ S(\d+)/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - const title = parseTitle($item) - const [, episode] = title.match(/ Ep (\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseDuration($item) { - let duration = $item('td:nth-child(3)').text() - const match = duration.match(/(\d{2}):(\d{2})/) - const hours = parseInt(match[1]) - const minutes = parseInt(match[2]) - - return hours * 60 + minutes -} - -function parseStart($item, date) { - let time = $item('td:nth-child(1)').text() - time = `${date.format('DD/MM/YYYY')} ${time}` - - return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') -} - -function parseTitle($item) { - return $item('td:nth-child(2) > a').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('tr[valign="top"]').toArray() -} - -function parsePages(content) { - const $ = cheerio.load(content) - const links = $('#schedule > div.schedule_search_result_container > div.box.well > a') - .map((i, el) => { - return $(el).attr('href') - }) - .get() - - return _.uniq(links) -} - -function loadNextPage(url, cookies) { - return axios - .get(url, { headers: { Cookie: cookies }, timeout: 30000 }) - .then(r => r.data) - .catch(err => { - console.log(err.message) - - return null - }) -} - -function loadLangCookies(channel) { - const languages = { - en: 'english', - id: 'indonesia' - } - const url = `https://www.mncvision.id/language_switcher/setlang/${languages[channel.lang]}/` - - return axios - .get(url, { timeout: 30000 }) - .then(r => parseCookies(r.headers)) - .catch(error => console.log(error.message)) -} - -async function loadDescription($item, cookies) { - const url = $item('a').attr('href') - if (!url) return null - const content = await axios - .get(url, { - headers: { 'X-Requested-With': 'XMLHttpRequest', Cookie: cookies }, - timeout: 30000 - }) - .then(r => r.data) - .catch(error => console.log(error.message)) - if (!content) return null - - const $page = cheerio.load(content) - const description = $page('.synopsis').text().trim() - - return description !== '-' ? description : null -} - -function parseCookies(headers) { - return Array.isArray(headers['set-cookie']) ? headers['set-cookie'].join(';') : null -} +const _ = require('lodash') +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mncvision.id', + days: 2, + url: 'https://mncvision.id/schedule/table', + request: { + method: 'POST', + data: function ({ channel, date }) { + const formData = new URLSearchParams() + formData.append('search_model', 'channel') + formData.append('af0rmelement', 'aformelement') + formData.append('fdate', date.format('YYYY-MM-DD')) + formData.append('fchannel', channel.site_id) + formData.append('submit', 'Search') + + return formData + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + jar: null + }, + async parser({ content, date, headers, channel }) { + const programs = [] + const cookies = parseCookies(headers) + if (!cookies) return programs + let items = parseItems(content) + if (!items.length) return programs + + const pages = parsePages(content) + for (let url of pages) { + items = items.concat(parseItems(await loadNextPage(url, cookies))) + } + + const langCookies = await loadLangCookies(channel) + if (!langCookies) return programs + + for (const item of items) { + const $item = cheerio.load(item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + const description = await loadDescription($item, langCookies) + programs.push({ + title: parseTitle($item), + season: parseSeason($item), + episode: parseEpisode($item), + description, + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://www.mncvision.id/schedule') + .then(response => response.data) + .catch(console.error) + + const $ = cheerio.load(data) + const items = $('select[name="fchannel"] option').toArray() + const channels = items.map(item => { + const $item = cheerio.load(item) + + return { + lang: 'id', + site_id: $item('*').attr('value'), + name: $item('*').text() + } + }) + + return channels + } +} + +function parseSeason($item) { + const title = parseTitle($item) + const [, season] = title.match(/ S(\d+)/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + const title = parseTitle($item) + const [, episode] = title.match(/ Ep (\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseDuration($item) { + let duration = $item('td:nth-child(3)').text() + const match = duration.match(/(\d{2}):(\d{2})/) + const hours = parseInt(match[1]) + const minutes = parseInt(match[2]) + + return hours * 60 + minutes +} + +function parseStart($item, date) { + let time = $item('td:nth-child(1)').text() + time = `${date.format('DD/MM/YYYY')} ${time}` + + return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') +} + +function parseTitle($item) { + return $item('td:nth-child(2) > a').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('tr[valign="top"]').toArray() +} + +function parsePages(content) { + const $ = cheerio.load(content) + const links = $('#schedule > div.schedule_search_result_container > div.box.well > a') + .map((i, el) => { + return $(el).attr('href') + }) + .get() + + return _.uniq(links) +} + +function loadNextPage(url, cookies) { + return axios + .get(url, { headers: { Cookie: cookies }, timeout: 30000 }) + .then(r => r.data) + .catch(err => { + console.log(err.message) + + return null + }) +} + +function loadLangCookies(channel) { + const languages = { + en: 'english', + id: 'indonesia' + } + const url = `https://www.mncvision.id/language_switcher/setlang/${languages[channel.lang]}/` + + return axios + .get(url, { timeout: 30000 }) + .then(r => parseCookies(r.headers)) + .catch(error => console.log(error.message)) +} + +async function loadDescription($item, cookies) { + const url = $item('a').attr('href') + if (!url) return null + const content = await axios + .get(url, { + headers: { 'X-Requested-With': 'XMLHttpRequest', Cookie: cookies }, + timeout: 30000 + }) + .then(r => r.data) + .catch(error => console.log(error.message)) + if (!content) return null + + const $page = cheerio.load(content) + const description = $page('.synopsis').text().trim() + + return description !== '-' ? description : null +} + +function parseCookies(headers) { + return Array.isArray(headers['set-cookie']) ? headers['set-cookie'].join(';') : null +} diff --git a/sites/mncvision.id/mncvision.id.test.js b/sites/mncvision.id/mncvision.id.test.js index 81995497b..712df202e 100644 --- a/sites/mncvision.id/mncvision.id.test.js +++ b/sites/mncvision.id/mncvision.id.test.js @@ -1,150 +1,150 @@ -// npm run channels:parse -- --config=./sites/mncvision.id/mncvision.id.config.js --output=./sites/mncvision.id/mncvision.id.channels.xml -// npm run grab -- --site=mncvision.id - -const { parser, url, request } = require('./mncvision.id.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-05', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '38', - xmltv_id: 'MiaoMi.hk', - lang: 'id' -} -const headers = { - 'set-cookie': [ - 's1nd0vL=05e9pr6gi112tdmutsn7big93o75r0b0; expires=Wed, 05-Oct-2022 14:18:22 GMT; Max-Age=7200; path=/; HttpOnly' - ] -} - -it('can generate valid url', () => { - expect(url).toBe('https://mncvision.id/schedule/table') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ channel, date }) - expect(data.get('search_model')).toBe('channel') - expect(data.get('af0rmelement')).toBe('aformelement') - expect(data.get('fdate')).toBe('2022-10-05') - expect(data.get('fchannel')).toBe('38') - expect(data.get('submit')).toBe('Search') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const indonesiaHeaders = { - 'set-cookie': [ - 's1nd0vL=e3vjb0oaf9vijiqsg7cml4i7fdkq16db; expires=Wed, 05-Oct-2022 14:54:16 GMT; Max-Age=7200; path=/; HttpOnly' - ] - } - const englishHeaders = { - 'set-cookie': [ - 's1nd0vL=hfd6hpnpr6gvgart0d8rf7ef6t4gi7nr; expires=Wed, 05-Oct-2022 15:08:55 GMT; Max-Age=7200; path=/; HttpOnly' - ] - } - axios.get.mockImplementation((url, opts) => { - if ( - url === 'https://www.mncvision.id/schedule/table/startno/50' && - opts.headers['Cookie'] === headers['set-cookie'][0] - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_p2.html')) - }) - } else if (url === 'https://www.mncvision.id/language_switcher/setlang/indonesia/') { - return Promise.resolve({ - headers: indonesiaHeaders - }) - } else if (url === 'https://www.mncvision.id/language_switcher/setlang/english/') { - return Promise.resolve({ - headers: englishHeaders - }) - } else if ( - url === - 'https://mncvision.id/schedule/detail/2022100500000038/Adventures-With-Miao-Mi-Ep-1/1' && - opts.headers['Cookie'] === indonesiaHeaders['set-cookie'][0] - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program_id.html')) - }) - } else if ( - url === - 'https://mncvision.id/schedule/detail/2022100500000038/Adventures-With-Miao-Mi-Ep-1/1' && - opts.headers['Cookie'] === englishHeaders['set-cookie'][0] - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) - }) - } - - return Promise.resolve({ data: '' }) - }) - - let indonesiaResults = await parser({ date, content, channel, headers }) - indonesiaResults = indonesiaResults.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(indonesiaResults[0]).toMatchObject({ - start: '2022-10-04T17:00:00.000Z', - stop: '2022-10-04T17:06:00.000Z', - title: 'Adventures With Miao Mi, Ep 1', - episode: 1, - description: - 'Ketika anak-anak mulai menghilang, sekelompok anak kecil harus menghadapi ketakutan terbesar mereka ketika mereka melawan sesosok badut pembunuh yang jahat.' - }) - - expect(indonesiaResults[4]).toMatchObject({ - start: '2022-10-04T17:33:00.000Z', - stop: '2022-10-04T17:46:00.000Z', - title: 'Leo Wildlife Ranger S2, Ep 27', - season: 2, - episode: 27 - }) - - let englishResults = await parser({ date, content, channel: { ...channel, lang: 'en' }, headers }) - englishResults = englishResults.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(englishResults[0]).toMatchObject({ - start: '2022-10-04T17:00:00.000Z', - stop: '2022-10-04T17:06:00.000Z', - title: 'Adventures With Miao Mi, Ep 1', - episode: 1, - description: - 'When children begin to disappear, a group of young kids have to face their biggest fears when they square off against a murderous, evil clown.' - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - let results = await parser({ - date, - channel, - content, - headers - }) - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/mncvision.id/mncvision.id.config.js --output=./sites/mncvision.id/mncvision.id.channels.xml +// npm run grab -- --site=mncvision.id + +const { parser, url, request } = require('./mncvision.id.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-05', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '38', + xmltv_id: 'MiaoMi.hk', + lang: 'id' +} +const headers = { + 'set-cookie': [ + 's1nd0vL=05e9pr6gi112tdmutsn7big93o75r0b0; expires=Wed, 05-Oct-2022 14:18:22 GMT; Max-Age=7200; path=/; HttpOnly' + ] +} + +it('can generate valid url', () => { + expect(url).toBe('https://mncvision.id/schedule/table') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ channel, date }) + expect(data.get('search_model')).toBe('channel') + expect(data.get('af0rmelement')).toBe('aformelement') + expect(data.get('fdate')).toBe('2022-10-05') + expect(data.get('fchannel')).toBe('38') + expect(data.get('submit')).toBe('Search') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const indonesiaHeaders = { + 'set-cookie': [ + 's1nd0vL=e3vjb0oaf9vijiqsg7cml4i7fdkq16db; expires=Wed, 05-Oct-2022 14:54:16 GMT; Max-Age=7200; path=/; HttpOnly' + ] + } + const englishHeaders = { + 'set-cookie': [ + 's1nd0vL=hfd6hpnpr6gvgart0d8rf7ef6t4gi7nr; expires=Wed, 05-Oct-2022 15:08:55 GMT; Max-Age=7200; path=/; HttpOnly' + ] + } + axios.get.mockImplementation((url, opts) => { + if ( + url === 'https://www.mncvision.id/schedule/table/startno/50' && + opts.headers['Cookie'] === headers['set-cookie'][0] + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_p2.html')) + }) + } else if (url === 'https://www.mncvision.id/language_switcher/setlang/indonesia/') { + return Promise.resolve({ + headers: indonesiaHeaders + }) + } else if (url === 'https://www.mncvision.id/language_switcher/setlang/english/') { + return Promise.resolve({ + headers: englishHeaders + }) + } else if ( + url === + 'https://mncvision.id/schedule/detail/2022100500000038/Adventures-With-Miao-Mi-Ep-1/1' && + opts.headers['Cookie'] === indonesiaHeaders['set-cookie'][0] + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_id.html')) + }) + } else if ( + url === + 'https://mncvision.id/schedule/detail/2022100500000038/Adventures-With-Miao-Mi-Ep-1/1' && + opts.headers['Cookie'] === englishHeaders['set-cookie'][0] + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) + }) + } + + return Promise.resolve({ data: '' }) + }) + + let indonesiaResults = await parser({ date, content, channel, headers }) + indonesiaResults = indonesiaResults.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(indonesiaResults[0]).toMatchObject({ + start: '2022-10-04T17:00:00.000Z', + stop: '2022-10-04T17:06:00.000Z', + title: 'Adventures With Miao Mi, Ep 1', + episode: 1, + description: + 'Ketika anak-anak mulai menghilang, sekelompok anak kecil harus menghadapi ketakutan terbesar mereka ketika mereka melawan sesosok badut pembunuh yang jahat.' + }) + + expect(indonesiaResults[4]).toMatchObject({ + start: '2022-10-04T17:33:00.000Z', + stop: '2022-10-04T17:46:00.000Z', + title: 'Leo Wildlife Ranger S2, Ep 27', + season: 2, + episode: 27 + }) + + let englishResults = await parser({ date, content, channel: { ...channel, lang: 'en' }, headers }) + englishResults = englishResults.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(englishResults[0]).toMatchObject({ + start: '2022-10-04T17:00:00.000Z', + stop: '2022-10-04T17:06:00.000Z', + title: 'Adventures With Miao Mi, Ep 1', + episode: 1, + description: + 'When children begin to disappear, a group of young kids have to face their biggest fears when they square off against a murderous, evil clown.' + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + let results = await parser({ + date, + channel, + content, + headers + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/moji.id/moji.id.config.js b/sites/moji.id/moji.id.config.js index d5656ce1b..2a942104f 100644 --- a/sites/moji.id/moji.id.config.js +++ b/sites/moji.id/moji.id.config.js @@ -1,122 +1,122 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const currentYear = new Date().getFullYear() - -module.exports = { - site: 'moji.id', - days: 4, - output: 'moji.id.guide.xml', - channels: 'moji.id.channels.xml', - lang: 'en', - delay: 5000, - - url: function () { - return 'https://moji.id/schedule' - }, - - request: { - method: 'GET', - timeout: 5000, - cache: { ttl: 60 * 60 * 1000 }, - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - } - }, - - logo: function (context) { - return context.channel.logo - }, - - parser: function (context) { - const programs = [] - const items = parseItems(context) - - items.forEach(item => { - programs.push({ - title: item.progTitle, - description: item.progDesc, - start: item.progStart, - stop: item.progStop - }) - }) - - return programs - } -} - -function parseItems(context) { - const $ = cheerio.load(context.content) - const schDayMonths = $('.date-slider .month').toArray() - const schPrograms = $('.desc-slider .list-slider').toArray() - const monthDate = dayjs(context.date).format('MMM DD') - const items = [] - - schDayMonths.forEach(function (schDayMonth, i) { - if (monthDate == $(schDayMonth).text()) { - let schDayPrograms = $(schPrograms[i]).find('.accordion').toArray() - schDayPrograms.forEach(function (program, i) { - let itemDay = { - progStart: parseStart(schDayMonth, program), - progStop: parseStop(schDayMonth, program, schDayPrograms[i + 1]), - progTitle: parseTitle(program), - progDesc: parseDescription(program) - } - items.push(itemDay) - }) - } - }) - - return items -} - -function parseTitle(item) { - return cheerio.load(item)('.name-prog').text() -} - -function parseDescription(item) { - return cheerio.load(item)('.content-acc span').text() -} - -function parseStart(schDayMonth, item) { - let monthDate = cheerio.load(schDayMonth).text().split(' ') - let startTime = cheerio.load(item)('.pkl').text() - let progStart = dayjs.tz( - currentYear + ' ' + monthDate[0] + ' ' + monthDate[1] + ' ' + startTime, - 'YYYY MMM DD HH:mm', - 'Asia/Jakarta' - ) - return progStart -} - -function parseStop(schDayMonth, itemCurrent, itemNext) { - let monthDate = cheerio.load(schDayMonth).text().split(' ') - - if (itemNext) { - let stopTime = cheerio.load(itemNext)('.pkl').text() - return dayjs.tz( - currentYear + ' ' + monthDate[0] + ' ' + monthDate[1] + ' ' + stopTime, - 'YYYY MMM DD HH:mm', - 'Asia/Jakarta' - ) - } else { - return dayjs.tz( - currentYear + - ' ' + - monthDate[0] + - ' ' + - (parseInt(monthDate[1]) + 1).toString().padStart(2, '0') + - ' 00:00', - 'YYYY MMM DD HH:mm', - 'Asia/Jakarta' - ) - } -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const currentYear = new Date().getFullYear() + +module.exports = { + site: 'moji.id', + days: 4, + output: 'moji.id.guide.xml', + channels: 'moji.id.channels.xml', + lang: 'en', + delay: 5000, + + url: function () { + return 'https://moji.id/schedule' + }, + + request: { + method: 'GET', + timeout: 5000, + cache: { ttl: 60 * 60 * 1000 }, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + } + }, + + logo: function (context) { + return context.channel.logo + }, + + parser: function (context) { + const programs = [] + const items = parseItems(context) + + items.forEach(item => { + programs.push({ + title: item.progTitle, + description: item.progDesc, + start: item.progStart, + stop: item.progStop + }) + }) + + return programs + } +} + +function parseItems(context) { + const $ = cheerio.load(context.content) + const schDayMonths = $('.date-slider .month').toArray() + const schPrograms = $('.desc-slider .list-slider').toArray() + const monthDate = dayjs(context.date).format('MMM DD') + const items = [] + + schDayMonths.forEach(function (schDayMonth, i) { + if (monthDate == $(schDayMonth).text()) { + let schDayPrograms = $(schPrograms[i]).find('.accordion').toArray() + schDayPrograms.forEach(function (program, i) { + let itemDay = { + progStart: parseStart(schDayMonth, program), + progStop: parseStop(schDayMonth, program, schDayPrograms[i + 1]), + progTitle: parseTitle(program), + progDesc: parseDescription(program) + } + items.push(itemDay) + }) + } + }) + + return items +} + +function parseTitle(item) { + return cheerio.load(item)('.name-prog').text() +} + +function parseDescription(item) { + return cheerio.load(item)('.content-acc span').text() +} + +function parseStart(schDayMonth, item) { + let monthDate = cheerio.load(schDayMonth).text().split(' ') + let startTime = cheerio.load(item)('.pkl').text() + let progStart = dayjs.tz( + currentYear + ' ' + monthDate[0] + ' ' + monthDate[1] + ' ' + startTime, + 'YYYY MMM DD HH:mm', + 'Asia/Jakarta' + ) + return progStart +} + +function parseStop(schDayMonth, itemCurrent, itemNext) { + let monthDate = cheerio.load(schDayMonth).text().split(' ') + + if (itemNext) { + let stopTime = cheerio.load(itemNext)('.pkl').text() + return dayjs.tz( + currentYear + ' ' + monthDate[0] + ' ' + monthDate[1] + ' ' + stopTime, + 'YYYY MMM DD HH:mm', + 'Asia/Jakarta' + ) + } else { + return dayjs.tz( + currentYear + + ' ' + + monthDate[0] + + ' ' + + (parseInt(monthDate[1]) + 1).toString().padStart(2, '0') + + ' 00:00', + 'YYYY MMM DD HH:mm', + 'Asia/Jakarta' + ) + } +} diff --git a/sites/moji.id/moji.id.test.js b/sites/moji.id/moji.id.test.js index 511827325..93c591ae0 100644 --- a/sites/moji.id/moji.id.test.js +++ b/sites/moji.id/moji.id.test.js @@ -1,38 +1,38 @@ -// npm run grab -- --site=moji.id -// npx jest moji.id.test.js - -const { url, parser } = require('./moji.id.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-08-18', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0', - xmltv_id: 'moji.id', - lang: 'en', - logo: 'https://moji.id/site/uploads/logo/62f9387ce00a2-224-x-71.png' -} - -const content = - '

    schedule

    FriAug 18
    SatAug 19
    SunAug 20
    Jam TayangProgram
    00:00TRUST
    Informasi seputar menjaga vitalitas pria
    00:302023 AVC CHALLENGE CUP FOR WOMEN (RECORDED)
    India Vs. Vietnam
    02:30ONE CHAMPIONSHIP 2021
    Siaran laga-laga pertandingan tinju gaya bebas internasional. Meyuguhkan pertarungan sengit dari para petarung profeisional kelas dunia.
    03:30VOLLEYBALL NATION\'S LEAGUE 2023 (RECORDED)
    TURKI vs BRAZIL
    05:00MOJI SPORT
    MOJI SPORT
    06:15LIPUTAN 6 PAGI MOJI
    Kompilasi ragam berita hard news dan soft news baik dari dalam negeri maupun internasional juga info prediksi cuaca di wilayah Indonesia
    07:00UNGKAP
    Liputan investigasi seputar berbagai topik dan peristiwa hangat serta kontroversial yang terjadi di Indonesia
    08:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    10:30SERIES PAGI
    GANTENG GANTENG SERIGALA
    12:30DIAM-DIAM SUKA
    DIAM-DIAM SUKA
    13:30PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    16:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    18:00PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    20:00MOJI DRAMA (CHHOTI SARDARNI)
    CHHOTI SARDARNI
    21:30SINEMA MALAM (BIDADARI CANTIK DI RUMAH KOST)
    (BIDADARI CANTIK DI RUMAH KOST
    23:00TRUST
    Informasi seputar menjaga vitalitas pria
    23:30TRUST
    Informasi seputar menjaga vitalitas pria
    ' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://moji.id/schedule') -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - expect(results).toMatchObject([]) -}) - -it('can parse response', () => { - const results = parser({ content: content, date: date }) - - expect(results[0]).toMatchObject({ - title: 'TRUST', - start: dayjs.tz('2023 Aug 18 00:00', 'YYYY MMM DD HH:mm', 'Asia/Jakarta'), - stop: dayjs.tz('2023 Aug 18 00:30', 'YYYY MMM DD HH:mm', 'Asia/Jakarta'), - description: 'Informasi seputar menjaga vitalitas pria' - }) -}) +// npm run grab -- --site=moji.id +// npx jest moji.id.test.js + +const { url, parser } = require('./moji.id.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-08-18', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0', + xmltv_id: 'moji.id', + lang: 'en', + logo: 'https://moji.id/site/uploads/logo/62f9387ce00a2-224-x-71.png' +} + +const content = + '

    schedule

    FriAug 18
    SatAug 19
    SunAug 20
    Jam TayangProgram
    00:00TRUST
    Informasi seputar menjaga vitalitas pria
    00:302023 AVC CHALLENGE CUP FOR WOMEN (RECORDED)
    India Vs. Vietnam
    02:30ONE CHAMPIONSHIP 2021
    Siaran laga-laga pertandingan tinju gaya bebas internasional. Meyuguhkan pertarungan sengit dari para petarung profeisional kelas dunia.
    03:30VOLLEYBALL NATION\'S LEAGUE 2023 (RECORDED)
    TURKI vs BRAZIL
    05:00MOJI SPORT
    MOJI SPORT
    06:15LIPUTAN 6 PAGI MOJI
    Kompilasi ragam berita hard news dan soft news baik dari dalam negeri maupun internasional juga info prediksi cuaca di wilayah Indonesia
    07:00UNGKAP
    Liputan investigasi seputar berbagai topik dan peristiwa hangat serta kontroversial yang terjadi di Indonesia
    08:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    10:30SERIES PAGI
    GANTENG GANTENG SERIGALA
    12:30DIAM-DIAM SUKA
    DIAM-DIAM SUKA
    13:30PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    16:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    18:00PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    20:00MOJI DRAMA (CHHOTI SARDARNI)
    CHHOTI SARDARNI
    21:30SINEMA MALAM (BIDADARI CANTIK DI RUMAH KOST)
    (BIDADARI CANTIK DI RUMAH KOST
    23:00TRUST
    Informasi seputar menjaga vitalitas pria
    23:30TRUST
    Informasi seputar menjaga vitalitas pria
    ' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://moji.id/schedule') +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + expect(results).toMatchObject([]) +}) + +it('can parse response', () => { + const results = parser({ content: content, date: date }) + + expect(results[0]).toMatchObject({ + title: 'TRUST', + start: dayjs.tz('2023 Aug 18 00:00', 'YYYY MMM DD HH:mm', 'Asia/Jakarta'), + stop: dayjs.tz('2023 Aug 18 00:30', 'YYYY MMM DD HH:mm', 'Asia/Jakarta'), + description: 'Informasi seputar menjaga vitalitas pria' + }) +}) diff --git a/sites/mon-programme-tv.be/mon-programme-tv.be.config.js b/sites/mon-programme-tv.be/mon-programme-tv.be.config.js index 72c8f6b37..1c3591447 100644 --- a/sites/mon-programme-tv.be/mon-programme-tv.be.config.js +++ b/sites/mon-programme-tv.be/mon-programme-tv.be.config.js @@ -1,100 +1,100 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'mon-programme-tv.be', - days: 2, - url({ date, channel }) { - return `https://www.mon-programme-tv.be/chaine/${date.format('DDMMYYYY')}/${ - channel.site_id - }.html` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - category: parseCategory($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.mon-programme-tv.be/chaine/toutes-les-chaines-television.html') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - - const channels = [] - $('.list-chaines > ul > li').each((i, el) => { - const [, site_id] = $(el) - .find('a') - .attr('href') - .match(/\/chaine\/(.*).html/) || [null, null] - const [, name] = $(el) - .find('a') - .attr('title') - .match(/Programme TV ce soir (.*)/) || [null, null] - - channels.push({ - site_id, - name, - lang: 'fr' - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseDescription($item) { - return $item('.episode').text().trim() -} - -function parseCategory($item) { - return $item('.type').text().trim() -} - -function parseIcon($item) { - return $item('.image img').data('src') -} - -function parseStart($item, date) { - const time = $item('.hour').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Brussels') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.box').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'mon-programme-tv.be', + days: 2, + url({ date, channel }) { + return `https://www.mon-programme-tv.be/chaine/${date.format('DDMMYYYY')}/${ + channel.site_id + }.html` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + category: parseCategory($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.mon-programme-tv.be/chaine/toutes-les-chaines-television.html') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + + const channels = [] + $('.list-chaines > ul > li').each((i, el) => { + const [, site_id] = $(el) + .find('a') + .attr('href') + .match(/\/chaine\/(.*).html/) || [null, null] + const [, name] = $(el) + .find('a') + .attr('title') + .match(/Programme TV ce soir (.*)/) || [null, null] + + channels.push({ + site_id, + name, + lang: 'fr' + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseDescription($item) { + return $item('.episode').text().trim() +} + +function parseCategory($item) { + return $item('.type').text().trim() +} + +function parseIcon($item) { + return $item('.image img').data('src') +} + +function parseStart($item, date) { + const time = $item('.hour').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Brussels') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.box').toArray() +} diff --git a/sites/mon-programme-tv.be/mon-programme-tv.be.test.js b/sites/mon-programme-tv.be/mon-programme-tv.be.test.js index c9f56deb3..9f78271dc 100644 --- a/sites/mon-programme-tv.be/mon-programme-tv.be.test.js +++ b/sites/mon-programme-tv.be/mon-programme-tv.be.test.js @@ -1,66 +1,66 @@ -// npm run channels:parse -- --config=./sites/mon-programme-tv.be/mon-programme-tv.be.config.js --output=./sites/mon-programme-tv.be/mon-programme-tv.be.channels.xml -// npm run grab -- --site=mon-programme-tv.be - -const { parser, url } = require('./mon-programme-tv.be.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1873/programme-television-ln24', - xmltv_id: 'LN24.be' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.mon-programme-tv.be/chaine/19012023/1873/programme-television-ln24.html' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-19T05:30:00.000Z', - stop: '2023-01-19T05:55:00.000Z', - title: 'LN Matin', - category: 'Magazine Actualité', - icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Reportage_1.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2023-01-19T05:55:00.000Z', - stop: '2023-01-19T06:00:00.000Z', - title: 'Météo', - category: 'Météo', - icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Meteo.jpg' - }) - - expect(results[8]).toMatchObject({ - start: '2023-01-19T08:00:00.000Z', - stop: '2023-01-19T08:05:00.000Z', - title: 'Le journal', - description: "L'information de la mi-journée avec des JT...", - category: 'Journal', - icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/journal.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - date - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/mon-programme-tv.be/mon-programme-tv.be.config.js --output=./sites/mon-programme-tv.be/mon-programme-tv.be.channels.xml +// npm run grab -- --site=mon-programme-tv.be + +const { parser, url } = require('./mon-programme-tv.be.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1873/programme-television-ln24', + xmltv_id: 'LN24.be' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.mon-programme-tv.be/chaine/19012023/1873/programme-television-ln24.html' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-19T05:30:00.000Z', + stop: '2023-01-19T05:55:00.000Z', + title: 'LN Matin', + category: 'Magazine Actualité', + icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Reportage_1.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2023-01-19T05:55:00.000Z', + stop: '2023-01-19T06:00:00.000Z', + title: 'Météo', + category: 'Météo', + icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Meteo.jpg' + }) + + expect(results[8]).toMatchObject({ + start: '2023-01-19T08:00:00.000Z', + stop: '2023-01-19T08:05:00.000Z', + title: 'Le journal', + description: "L'information de la mi-journée avec des JT...", + category: 'Journal', + icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/journal.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/movistarplus.es/movistarplus.es.config.js b/sites/movistarplus.es/movistarplus.es.config.js index d0c1f9acb..a6493c718 100644 --- a/sites/movistarplus.es/movistarplus.es.config.js +++ b/sites/movistarplus.es/movistarplus.es.config.js @@ -1,49 +1,49 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'movistarplus.es', - days: 2, - url: function ({ date }) { - return `https://www.movistarplus.es/programacion-tv/${date.format('YYYY-MM-DD')}?v=json` - }, - parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - let guideDate = date - items.forEach(item => { - let startTime = DateTime.fromFormat( - `${guideDate.format('YYYY-MM-DD')} ${item.HORA_INICIO}`, - 'yyyy-MM-dd HH:mm', - { - zone: 'Europe/Madrid' - } - ).toUTC() - let stopTime = DateTime.fromFormat( - `${guideDate.format('YYYY-MM-DD')} ${item.HORA_FIN}`, - 'yyyy-MM-dd HH:mm', - { - zone: 'Europe/Madrid' - } - ).toUTC() - if (stopTime < startTime) { - guideDate = guideDate.add(1, 'd') - stopTime = stopTime.plus({ days: 1 }) - } - programs.push({ - title: item.TITULO, - category: item.GENERO, - start: startTime, - stop: stopTime - }) - }) - return programs - } -} - -function parseItems(content, channel) { - const json = typeof content === 'string' ? JSON.parse(content) : content - if (!(`${channel.site_id}-CODE` in json.data)) return [] - const data = json.data[`${channel.site_id}-CODE`] - return data ? data.PROGRAMAS : [] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'movistarplus.es', + days: 2, + url: function ({ date }) { + return `https://www.movistarplus.es/programacion-tv/${date.format('YYYY-MM-DD')}?v=json` + }, + parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + let guideDate = date + items.forEach(item => { + let startTime = DateTime.fromFormat( + `${guideDate.format('YYYY-MM-DD')} ${item.HORA_INICIO}`, + 'yyyy-MM-dd HH:mm', + { + zone: 'Europe/Madrid' + } + ).toUTC() + let stopTime = DateTime.fromFormat( + `${guideDate.format('YYYY-MM-DD')} ${item.HORA_FIN}`, + 'yyyy-MM-dd HH:mm', + { + zone: 'Europe/Madrid' + } + ).toUTC() + if (stopTime < startTime) { + guideDate = guideDate.add(1, 'd') + stopTime = stopTime.plus({ days: 1 }) + } + programs.push({ + title: item.TITULO, + category: item.GENERO, + start: startTime, + stop: stopTime + }) + }) + return programs + } +} + +function parseItems(content, channel) { + const json = typeof content === 'string' ? JSON.parse(content) : content + if (!(`${channel.site_id}-CODE` in json.data)) return [] + const data = json.data[`${channel.site_id}-CODE`] + return data ? data.PROGRAMAS : [] +} diff --git a/sites/movistarplus.es/movistarplus.es.test.js b/sites/movistarplus.es/movistarplus.es.test.js index 88f665b29..de8245780 100644 --- a/sites/movistarplus.es/movistarplus.es.test.js +++ b/sites/movistarplus.es/movistarplus.es.test.js @@ -1,57 +1,57 @@ -// npm run grab -- --site=movistarplus.es - -const { parser, url } = require('./movistarplus.es.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'TVE', - xmltv_id: 'SomeChannel.es' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.movistarplus.es/programacion-tv/2022-03-11?v=json') -}) - -it('can parse response', () => { - const content = - '{"success":"true","msg":"","data":{"TVE-CODE":{"DATOS_CADENA":{"CODIGO":"TVE","MARCA":"TVE","NOMBRE":"LA 1","URL":"https://www.movistarplus.es/canal?nombre=LA%2B1&id=TVE","DIAL_PRINCIPAL":["01"],"DIALES":[1],"UID":null,"CASID":null,"SERVICEUID":null,"SERVICEUID2":null,"SERVICEID":null,"ESVIRTUAL":null,"ESSATELITE":null,"UPSELLING":null,"puntoReproduccion":null},"PROGRAMAS":[{"DIRECTO":false,"TEMPORADA":"","TITULO":"Telediario Matinal","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":150,"DURACION_VISUAL":150,"HORA_INICIO":"06:00","HORA_FIN":"08:30","ELEMENTO":"1709045","EVENTO":"99422566","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/telediario-matinal?tipo=R&id=99422566"},{"DIRECTO":false,"TEMPORADA":"","TITULO":"Las Claves del Siglo XXI: Episodio 8","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":135,"DURACION_VISUAL":135,"HORA_INICIO":"22:15","HORA_FIN":"00:30","ELEMENTO":"2051356","EVENTO":"99422634","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/las-claves-del-siglo-xxi-t1/episodio-8?tipo=R&id=99422634"},{"DIRECTO":false,"TEMPORADA":"","TITULO":"Noticias 24H","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":170,"DURACION_VISUAL":170,"HORA_INICIO":"03:10","HORA_FIN":"06:00","ELEMENTO":"518403","EVENTO":"99422646","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/noticias-24h?tipo=R&id=99422646"}]}}}' - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2022-03-11T05:00:00.000Z', - stop: '2022-03-11T07:30:00.000Z', - category: 'Información', - title: 'Telediario Matinal' - }, - { - start: '2022-03-11T21:15:00.000Z', - stop: '2022-03-11T23:30:00.000Z', - category: 'Información', - title: 'Las Claves del Siglo XXI: Episodio 8' - }, - { - start: '2022-03-12T02:10:00.000Z', - stop: '2022-03-12T05:00:00.000Z', - category: 'Información', - title: 'Noticias 24H' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"success":"true","msg":"","data":{}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=movistarplus.es + +const { parser, url } = require('./movistarplus.es.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'TVE', + xmltv_id: 'SomeChannel.es' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.movistarplus.es/programacion-tv/2022-03-11?v=json') +}) + +it('can parse response', () => { + const content = + '{"success":"true","msg":"","data":{"TVE-CODE":{"DATOS_CADENA":{"CODIGO":"TVE","MARCA":"TVE","NOMBRE":"LA 1","URL":"https://www.movistarplus.es/canal?nombre=LA%2B1&id=TVE","DIAL_PRINCIPAL":["01"],"DIALES":[1],"UID":null,"CASID":null,"SERVICEUID":null,"SERVICEUID2":null,"SERVICEID":null,"ESVIRTUAL":null,"ESSATELITE":null,"UPSELLING":null,"puntoReproduccion":null},"PROGRAMAS":[{"DIRECTO":false,"TEMPORADA":"","TITULO":"Telediario Matinal","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":150,"DURACION_VISUAL":150,"HORA_INICIO":"06:00","HORA_FIN":"08:30","ELEMENTO":"1709045","EVENTO":"99422566","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/telediario-matinal?tipo=R&id=99422566"},{"DIRECTO":false,"TEMPORADA":"","TITULO":"Las Claves del Siglo XXI: Episodio 8","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":135,"DURACION_VISUAL":135,"HORA_INICIO":"22:15","HORA_FIN":"00:30","ELEMENTO":"2051356","EVENTO":"99422634","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/las-claves-del-siglo-xxi-t1/episodio-8?tipo=R&id=99422634"},{"DIRECTO":false,"TEMPORADA":"","TITULO":"Noticias 24H","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":170,"DURACION_VISUAL":170,"HORA_INICIO":"03:10","HORA_FIN":"06:00","ELEMENTO":"518403","EVENTO":"99422646","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/noticias-24h?tipo=R&id=99422646"}]}}}' + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2022-03-11T05:00:00.000Z', + stop: '2022-03-11T07:30:00.000Z', + category: 'Información', + title: 'Telediario Matinal' + }, + { + start: '2022-03-11T21:15:00.000Z', + stop: '2022-03-11T23:30:00.000Z', + category: 'Información', + title: 'Las Claves del Siglo XXI: Episodio 8' + }, + { + start: '2022-03-12T02:10:00.000Z', + stop: '2022-03-12T05:00:00.000Z', + category: 'Información', + title: 'Noticias 24H' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"success":"true","msg":"","data":{}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mtel.ba/mtel.ba.config.js b/sites/mtel.ba/mtel.ba.config.js index 33f13ab4b..aa355a5f4 100644 --- a/sites/mtel.ba/mtel.ba.config.js +++ b/sites/mtel.ba/mtel.ba.config.js @@ -1,58 +1,58 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(timezone) - -module.exports = { - site: 'mtel.ba', - days: 2, - url: function ({ channel, date }) { - const [position] = channel.site_id.split('#') - - return `https://mtel.ba/oec/epg/program?date=${date.format('YYYY-MM-DD')}&position=${position}` - }, - request: { - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (item.title === 'Nema informacija o programu') return - programs.push({ - title: item.title, - description: item.description, - category: item.category, - icon: item.image, - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.tz(item.full_start, 'Europe/Sarajevo') -} - -function parseStop(item) { - return dayjs.tz(item.full_end, 'Europe/Sarajevo') -} - -function parseContent(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return null - - return data.channels.find(i => i.id === channelId) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - - return data ? data.items : [] -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(timezone) + +module.exports = { + site: 'mtel.ba', + days: 2, + url: function ({ channel, date }) { + const [position] = channel.site_id.split('#') + + return `https://mtel.ba/oec/epg/program?date=${date.format('YYYY-MM-DD')}&position=${position}` + }, + request: { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (item.title === 'Nema informacija o programu') return + programs.push({ + title: item.title, + description: item.description, + category: item.category, + icon: item.image, + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.tz(item.full_start, 'Europe/Sarajevo') +} + +function parseStop(item) { + return dayjs.tz(item.full_end, 'Europe/Sarajevo') +} + +function parseContent(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return null + + return data.channels.find(i => i.id === channelId) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + + return data ? data.items : [] +} diff --git a/sites/mtel.ba/mtel.ba.test.js b/sites/mtel.ba/mtel.ba.test.js index 765979ee8..6b4598df8 100644 --- a/sites/mtel.ba/mtel.ba.test.js +++ b/sites/mtel.ba/mtel.ba.test.js @@ -1,53 +1,53 @@ -// npm run grab -- --site=mtel.ba - -const { parser, url, request } = require('./mtel.ba.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '001#11', - xmltv_id: 'RTRSTV.ba' -} -const content = - '{"page":0,"total_pages":1,"date":"2021-11-10","channels":[{"id":"11","name":"RTRS","description":null,"link":null,"image":"https://mtel.ba/oec/images/tv_channels/c3556aa629b00325aaaea622abfb1070.png","position":"001","items":[{"id_channel":"11","title":"\u0160uma","description":"Krajem decembra 1947. godine jugoslovenski predsjednik Josip Broz Tito prvi put je posjetio Rumuniju. Da bi u\u010dvrstili novo socijalisti\u010dko prijateljstvo, rumunski zvani\u010dnici su poklonili Titu sliku velikog rumunskog umjetnika Jona Andreskua pod nazivom \u0160uma. Mnogo godina kasnije ta slika je umje\u0161ana u napetu \u0161pijunsku pri\u010du i otkriva tajnu koja \u0107e uzdrmati temelje i Jugoslavije i Rumunije. Film je svjedok kompleksnosti i raznovrsnosti glasova koji \u010dine ono \u0161to zovemo stvarno\u0161\u0107u.","start":"00:00:00","duration":"46.00","full_start":"2021-11-09 23:29:00","full_end":"2021-11-10 00:46:00","image":"https://mtel.ba/oec/images/epg/60881491.jpg","category":"Televizijski film","subcategory":"Dokumentarna drama"},{"id_channel":"11","title":"Nema informacija o programu","description":"","start":"07:32:00","duration":"988.00","full_start":"2021-11-10 07:32:00","full_end":"2021-11-10 24:00:00","image":"","category":"","subcategory":""}]}]}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://mtel.ba/oec/epg/program?date=2021-11-10&position=001' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'X-Requested-With': 'XMLHttpRequest' - }) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-09T22:29:00.000Z', - stop: '2021-11-09T23:46:00.000Z', - title: 'Šuma', - icon: 'https://mtel.ba/oec/images/epg/60881491.jpg', - description: - 'Krajem decembra 1947. godine jugoslovenski predsjednik Josip Broz Tito prvi put je posjetio Rumuniju. Da bi učvrstili novo socijalističko prijateljstvo, rumunski zvaničnici su poklonili Titu sliku velikog rumunskog umjetnika Jona Andreskua pod nazivom Šuma. Mnogo godina kasnije ta slika je umješana u napetu špijunsku priču i otkriva tajnu koja će uzdrmati temelje i Jugoslavije i Rumunije. Film je svjedok kompleksnosti i raznovrsnosti glasova koji čine ono što zovemo stvarnošću.', - category: 'Televizijski film' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"message":"Tra\u017eeni termin nije prona\u0111en.\u003Cbr\u003E\u003Cbr\u003EProverite da li ste upisali pravilno ili poku\u0161ajte sa nekim drugim terminom."}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mtel.ba + +const { parser, url, request } = require('./mtel.ba.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '001#11', + xmltv_id: 'RTRSTV.ba' +} +const content = + '{"page":0,"total_pages":1,"date":"2021-11-10","channels":[{"id":"11","name":"RTRS","description":null,"link":null,"image":"https://mtel.ba/oec/images/tv_channels/c3556aa629b00325aaaea622abfb1070.png","position":"001","items":[{"id_channel":"11","title":"\u0160uma","description":"Krajem decembra 1947. godine jugoslovenski predsjednik Josip Broz Tito prvi put je posjetio Rumuniju. Da bi u\u010dvrstili novo socijalisti\u010dko prijateljstvo, rumunski zvani\u010dnici su poklonili Titu sliku velikog rumunskog umjetnika Jona Andreskua pod nazivom \u0160uma. Mnogo godina kasnije ta slika je umje\u0161ana u napetu \u0161pijunsku pri\u010du i otkriva tajnu koja \u0107e uzdrmati temelje i Jugoslavije i Rumunije. Film je svjedok kompleksnosti i raznovrsnosti glasova koji \u010dine ono \u0161to zovemo stvarno\u0161\u0107u.","start":"00:00:00","duration":"46.00","full_start":"2021-11-09 23:29:00","full_end":"2021-11-10 00:46:00","image":"https://mtel.ba/oec/images/epg/60881491.jpg","category":"Televizijski film","subcategory":"Dokumentarna drama"},{"id_channel":"11","title":"Nema informacija o programu","description":"","start":"07:32:00","duration":"988.00","full_start":"2021-11-10 07:32:00","full_end":"2021-11-10 24:00:00","image":"","category":"","subcategory":""}]}]}' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://mtel.ba/oec/epg/program?date=2021-11-10&position=001' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'X-Requested-With': 'XMLHttpRequest' + }) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-09T22:29:00.000Z', + stop: '2021-11-09T23:46:00.000Z', + title: 'Šuma', + icon: 'https://mtel.ba/oec/images/epg/60881491.jpg', + description: + 'Krajem decembra 1947. godine jugoslovenski predsjednik Josip Broz Tito prvi put je posjetio Rumuniju. Da bi učvrstili novo socijalističko prijateljstvo, rumunski zvaničnici su poklonili Titu sliku velikog rumunskog umjetnika Jona Andreskua pod nazivom Šuma. Mnogo godina kasnije ta slika je umješana u napetu špijunsku priču i otkriva tajnu koja će uzdrmati temelje i Jugoslavije i Rumunije. Film je svjedok kompleksnosti i raznovrsnosti glasova koji čine ono što zovemo stvarnošću.', + category: 'Televizijski film' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"message":"Tra\u017eeni termin nije prona\u0111en.\u003Cbr\u003E\u003Cbr\u003EProverite da li ste upisali pravilno ili poku\u0161ajte sa nekim drugim terminom."}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mts.rs/mts.rs.config.js b/sites/mts.rs/mts.rs.config.js index 00d3b552a..b13829806 100644 --- a/sites/mts.rs/mts.rs.config.js +++ b/sites/mts.rs/mts.rs.config.js @@ -1,63 +1,63 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'mts.rs', - days: 2, - url({ date, channel }) { - const [position] = channel.site_id.split('#') - - return `https://mts.rs/oec/epg/program?date=${date.format('YYYY-MM-DD')}&position=${position}` - }, - request: { - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }, - parser: function ({ content, channel }) { - let programs = [] - const data = parseContent(content, channel) - const items = parseItems(data) - items.forEach(item => { - programs.push({ - title: item.title, - category: item.category, - description: item.description, - icon: item.image, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseContent(content, channel) { - const [, site_id] = channel.site_id.split('#') - let data - try { - data = JSON.parse(content) - } catch (error) { - console.log(error) - } - if (!data || !data.channels || !data.channels.length) return null - - return data.channels.find(c => c.id === site_id) || null -} - -function parseStart(item) { - return dayjs.tz(item.full_start, 'Europe/Belgrade') -} - -function parseStop(item) { - return dayjs.tz(item.full_end, 'Europe/Belgrade') -} - -function parseItems(data) { - return data && Array.isArray(data.items) ? data.items : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'mts.rs', + days: 2, + url({ date, channel }) { + const [position] = channel.site_id.split('#') + + return `https://mts.rs/oec/epg/program?date=${date.format('YYYY-MM-DD')}&position=${position}` + }, + request: { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }, + parser: function ({ content, channel }) { + let programs = [] + const data = parseContent(content, channel) + const items = parseItems(data) + items.forEach(item => { + programs.push({ + title: item.title, + category: item.category, + description: item.description, + icon: item.image, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseContent(content, channel) { + const [, site_id] = channel.site_id.split('#') + let data + try { + data = JSON.parse(content) + } catch (error) { + console.log(error) + } + if (!data || !data.channels || !data.channels.length) return null + + return data.channels.find(c => c.id === site_id) || null +} + +function parseStart(item) { + return dayjs.tz(item.full_start, 'Europe/Belgrade') +} + +function parseStop(item) { + return dayjs.tz(item.full_end, 'Europe/Belgrade') +} + +function parseItems(data) { + return data && Array.isArray(data.items) ? data.items : [] +} diff --git a/sites/mts.rs/mts.rs.test.js b/sites/mts.rs/mts.rs.test.js index 87ff70ccc..836487c88 100644 --- a/sites/mts.rs/mts.rs.test.js +++ b/sites/mts.rs/mts.rs.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=mts.rs - -const { parser, url, request } = require('./mts.rs.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '101#597', - xmltv_id: 'RTS1.rs' -} -const content = - '{"page":0,"total_pages":1,"date":"2021-11-07","channels":[{"id":"597","name":"RTS 1","description":null,"link":null,"image":"https://mts.rs/oec/images/tv_channels/904ddd8cd6720a4a1c23eae513b5b957.jpg","position":"101","positions":"101","items":[{"id_channel":"597","title":"Zaboravljeni zlo\u010din","description":"Novinarka-fotoreporter, D\u017ein, istra\u017euje okrutno i senzacionalno, nere\u0161eno ubistvo sekirom iz davne 1873. godine. Ubistvo koje koincidira sa nedavnim identi\u010dnim brutalnim dvostrukim ubistvom. Zaplet se odvija izme\u0111u pri\u010de o\u010devica iz toga doba - pri\u010de iz novinske arhive i D\u017einine privatne borbe sa ljubomorom i sumnjom koje prate njen brak.","start":"00:00:00","duration":"103.00","full_start":"2021-11-06 23:44:00","full_end":"2021-11-07 01:43:00","image":"https://mts.rs/oec/images/epg/2_abb81cc24d8ce957eece50f991a31e59780e4e53_E7D8ECDE568E84E3C86CCDBDB647355E.jpg","category":"Bioskopski film","subcategory":""}]}]}' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://mts.rs/oec/epg/program?date=2021-11-07&position=101') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'X-Requested-With': 'XMLHttpRequest' - }) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-06T22:44:00.000Z', - stop: '2021-11-07T00:43:00.000Z', - title: 'Zaboravljeni zlo\u010din', - category: 'Bioskopski film', - icon: 'https://mts.rs/oec/images/epg/2_abb81cc24d8ce957eece50f991a31e59780e4e53_E7D8ECDE568E84E3C86CCDBDB647355E.jpg', - description: - 'Novinarka-fotoreporter, D\u017ein, istra\u017euje okrutno i senzacionalno, nere\u0161eno ubistvo sekirom iz davne 1873. godine. Ubistvo koje koincidira sa nedavnim identi\u010dnim brutalnim dvostrukim ubistvom. Zaplet se odvija izme\u0111u pri\u010de o\u010devica iz toga doba - pri\u010de iz novinske arhive i D\u017einine privatne borbe sa ljubomorom i sumnjom koje prate njen brak.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"message":"Nema rezultata."}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mts.rs + +const { parser, url, request } = require('./mts.rs.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '101#597', + xmltv_id: 'RTS1.rs' +} +const content = + '{"page":0,"total_pages":1,"date":"2021-11-07","channels":[{"id":"597","name":"RTS 1","description":null,"link":null,"image":"https://mts.rs/oec/images/tv_channels/904ddd8cd6720a4a1c23eae513b5b957.jpg","position":"101","positions":"101","items":[{"id_channel":"597","title":"Zaboravljeni zlo\u010din","description":"Novinarka-fotoreporter, D\u017ein, istra\u017euje okrutno i senzacionalno, nere\u0161eno ubistvo sekirom iz davne 1873. godine. Ubistvo koje koincidira sa nedavnim identi\u010dnim brutalnim dvostrukim ubistvom. Zaplet se odvija izme\u0111u pri\u010de o\u010devica iz toga doba - pri\u010de iz novinske arhive i D\u017einine privatne borbe sa ljubomorom i sumnjom koje prate njen brak.","start":"00:00:00","duration":"103.00","full_start":"2021-11-06 23:44:00","full_end":"2021-11-07 01:43:00","image":"https://mts.rs/oec/images/epg/2_abb81cc24d8ce957eece50f991a31e59780e4e53_E7D8ECDE568E84E3C86CCDBDB647355E.jpg","category":"Bioskopski film","subcategory":""}]}]}' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://mts.rs/oec/epg/program?date=2021-11-07&position=101') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'X-Requested-With': 'XMLHttpRequest' + }) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-06T22:44:00.000Z', + stop: '2021-11-07T00:43:00.000Z', + title: 'Zaboravljeni zlo\u010din', + category: 'Bioskopski film', + icon: 'https://mts.rs/oec/images/epg/2_abb81cc24d8ce957eece50f991a31e59780e4e53_E7D8ECDE568E84E3C86CCDBDB647355E.jpg', + description: + 'Novinarka-fotoreporter, D\u017ein, istra\u017euje okrutno i senzacionalno, nere\u0161eno ubistvo sekirom iz davne 1873. godine. Ubistvo koje koincidira sa nedavnim identi\u010dnim brutalnim dvostrukim ubistvom. Zaplet se odvija izme\u0111u pri\u010de o\u010devica iz toga doba - pri\u010de iz novinske arhive i D\u017einine privatne borbe sa ljubomorom i sumnjom koje prate njen brak.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"message":"Nema rezultata."}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mujtvprogram.cz/mujtvprogram.cz.config.js b/sites/mujtvprogram.cz/mujtvprogram.cz.config.js index 8e9f0caa5..1b83c1f65 100644 --- a/sites/mujtvprogram.cz/mujtvprogram.cz.config.js +++ b/sites/mujtvprogram.cz/mujtvprogram.cz.config.js @@ -1,69 +1,69 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const convert = require('xml-js') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mujtvprogram.cz', - days: 2, - url({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - return `https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=${channel.site_id}&day=${diff}` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name._text, - start: parseTime(item.startDate._text), - stop: parseTime(item.endDate._text), - description: parseDescription(item), - category: parseCategory(item), - date: item.year._text || null, - director: parseList(item.directors), - actor: parseList(item.actors) - }) - }) - return programs - } -} - -function parseItems(content) { - try { - const data = convert.xml2js(content, { - compact: true, - ignoreDeclaration: true, - ignoreAttributes: true - }) - if (!data) return [] - const programmes = data['tv-program-programmes'].programme - return programmes && Array.isArray(programmes) ? programmes : [] - } catch (err) { - return [] - } -} -function parseDescription(item) { - if (item.longDescription) return item.longDescription._text - if (item.shortDescription) return item.shortDescription._text - return null -} - -function parseList(list) { - if (!list) return [] - if (!list._text) return [] - return typeof list._text === 'string' ? list._text.split(', ') : [] -} -function parseTime(time) { - return dayjs.tz(time, 'DD.MM.YYYY HH.mm', 'Europe/Prague') -} - -function parseCategory(item) { - if (!item['programme-type']) return null - return item['programme-type'].name._text -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const convert = require('xml-js') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mujtvprogram.cz', + days: 2, + url({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + return `https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=${channel.site_id}&day=${diff}` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name._text, + start: parseTime(item.startDate._text), + stop: parseTime(item.endDate._text), + description: parseDescription(item), + category: parseCategory(item), + date: item.year._text || null, + director: parseList(item.directors), + actor: parseList(item.actors) + }) + }) + return programs + } +} + +function parseItems(content) { + try { + const data = convert.xml2js(content, { + compact: true, + ignoreDeclaration: true, + ignoreAttributes: true + }) + if (!data) return [] + const programmes = data['tv-program-programmes'].programme + return programmes && Array.isArray(programmes) ? programmes : [] + } catch (err) { + return [] + } +} +function parseDescription(item) { + if (item.longDescription) return item.longDescription._text + if (item.shortDescription) return item.shortDescription._text + return null +} + +function parseList(list) { + if (!list) return [] + if (!list._text) return [] + return typeof list._text === 'string' ? list._text.split(', ') : [] +} +function parseTime(time) { + return dayjs.tz(time, 'DD.MM.YYYY HH.mm', 'Europe/Prague') +} + +function parseCategory(item) { + if (!item['programme-type']) return null + return item['programme-type'].name._text +} diff --git a/sites/mujtvprogram.cz/mujtvprogram.cz.test.js b/sites/mujtvprogram.cz/mujtvprogram.cz.test.js index c15d23299..6cf656379 100644 --- a/sites/mujtvprogram.cz/mujtvprogram.cz.test.js +++ b/sites/mujtvprogram.cz/mujtvprogram.cz.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=mujtvprogram.cz - -const { parser, url } = require('./mujtvprogram.cz.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const channel = { - site_id: '1', - xmltv_id: 'CT1.cz' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=1' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results[3]).toMatchObject({ - title: 'Čepice', - description: - 'Jarka (J. Bohdalová) vyčítá manželovi Jiřímu (F. Řehák), že jí nepomáhá při předvánočním úklidu. Vzápětí ale náhodou najde ve skříni ukrytou dámskou čepici a napadne ji, že jde o Jiřího dárek pro ni pod stromeček. Její chování se ihned změní. Jen muži naznačí, že by chtěla čepici jiné barvy. Manžel jí ovšem řekne, že čepici si u něj schoval kamarád Venca (M. Šulc). Zklamaná žena to prozradí Vencově manželce Božce (A. Tománková). Na Štědrý den však Božka najde pod stromečkem jen rtěnku...', - category: 'film', - date: '1983', - director: ['Mudra F.'], - actor: ['Bohdalová J.', 'Řehák F.', 'Šulc M.'], - start: '2022-12-23T08:00:00.000Z', - stop: '2022-12-23T08:20:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser(content, channel) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mujtvprogram.cz + +const { parser, url } = require('./mujtvprogram.cz.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const channel = { + site_id: '1', + xmltv_id: 'CT1.cz' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=1' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results[3]).toMatchObject({ + title: 'Čepice', + description: + 'Jarka (J. Bohdalová) vyčítá manželovi Jiřímu (F. Řehák), že jí nepomáhá při předvánočním úklidu. Vzápětí ale náhodou najde ve skříni ukrytou dámskou čepici a napadne ji, že jde o Jiřího dárek pro ni pod stromeček. Její chování se ihned změní. Jen muži naznačí, že by chtěla čepici jiné barvy. Manžel jí ovšem řekne, že čepici si u něj schoval kamarád Venca (M. Šulc). Zklamaná žena to prozradí Vencově manželce Božce (A. Tománková). Na Štědrý den však Božka najde pod stromečkem jen rtěnku...', + category: 'film', + date: '1983', + director: ['Mudra F.'], + actor: ['Bohdalová J.', 'Řehák F.', 'Šulc M.'], + start: '2022-12-23T08:00:00.000Z', + stop: '2022-12-23T08:20:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser(content, channel) + expect(result).toMatchObject([]) +}) diff --git a/sites/musor.tv/musor.tv.config.js b/sites/musor.tv/musor.tv.config.js index 85568d9f7..6a58e6b4d 100644 --- a/sites/musor.tv/musor.tv.config.js +++ b/sites/musor.tv/musor.tv.config.js @@ -1,87 +1,87 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const axios = require('axios') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'musor.tv', - days: 2, - url({ channel, date }) { - return dayjs.utc().isSame(date, 'd') - ? `https://musor.tv/mai/tvmusor/${channel.site_id}` - : `https://musor.tv/napi/tvmusor/${channel.site_id}/${date.format('YYYY.MM.DD')}` - }, - parser({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item) - if (prev) prev.stop = start - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://musor.tv/') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const channels = $('body > div.big_content > div > nav > table > tbody > tr > td > a').toArray() - return channels - .map(item => { - const $item = cheerio.load(item) - const url = $item('*').attr('href') - if (!url.startsWith('//musor.tv/mai/tvmusor/')) return null - const site_id = url.replace('//musor.tv/mai/tvmusor/', '') - return { - lang: 'hu', - site_id, - name: $item('*').text() - } - }) - .filter(i => i) - } -} - -function parseIcon($item) { - const imgSrc = $item('div.smartpe_screenshot > img').attr('src') - - return imgSrc ? `https:${imgSrc}` : null -} - -function parseTitle($item) { - return $item('div:nth-child(2) > div > h3 > a').text().trim() -} - -function parseDescription($item) { - return $item('div:nth-child(5) > div > div').text().trim() -} - -function parseStart($item) { - let datetime = $item('div:nth-child(1) > div > div > div > div > time').attr('content') - if (!datetime) return null - - return dayjs.utc(datetime.replace('GMT', 'T'), 'YYYY-MM-DDTHH:mm:ss') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('div.multicolumndayprogarea > div.smartpe_progentry').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const axios = require('axios') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'musor.tv', + days: 2, + url({ channel, date }) { + return dayjs.utc().isSame(date, 'd') + ? `https://musor.tv/mai/tvmusor/${channel.site_id}` + : `https://musor.tv/napi/tvmusor/${channel.site_id}/${date.format('YYYY.MM.DD')}` + }, + parser({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item) + if (prev) prev.stop = start + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://musor.tv/') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const channels = $('body > div.big_content > div > nav > table > tbody > tr > td > a').toArray() + return channels + .map(item => { + const $item = cheerio.load(item) + const url = $item('*').attr('href') + if (!url.startsWith('//musor.tv/mai/tvmusor/')) return null + const site_id = url.replace('//musor.tv/mai/tvmusor/', '') + return { + lang: 'hu', + site_id, + name: $item('*').text() + } + }) + .filter(i => i) + } +} + +function parseIcon($item) { + const imgSrc = $item('div.smartpe_screenshot > img').attr('src') + + return imgSrc ? `https:${imgSrc}` : null +} + +function parseTitle($item) { + return $item('div:nth-child(2) > div > h3 > a').text().trim() +} + +function parseDescription($item) { + return $item('div:nth-child(5) > div > div').text().trim() +} + +function parseStart($item) { + let datetime = $item('div:nth-child(1) > div > div > div > div > time').attr('content') + if (!datetime) return null + + return dayjs.utc(datetime.replace('GMT', 'T'), 'YYYY-MM-DDTHH:mm:ss') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('div.multicolumndayprogarea > div.smartpe_progentry').toArray() +} diff --git a/sites/musor.tv/musor.tv.test.js b/sites/musor.tv/musor.tv.test.js index 6caa143af..4c07d8c2d 100644 --- a/sites/musor.tv/musor.tv.test.js +++ b/sites/musor.tv/musor.tv.test.js @@ -1,60 +1,60 @@ -// npm run channels:parse -- --config=./sites/musor.tv/musor.tv.config.js --output=./sites/musor.tv/musor.tv.channels.xml -// npm run grab -- --site=musor.tv - -const { parser, url } = require('./musor.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'HATOS_CSATORNA', - xmltv_id: 'Hatoscsatorna.hu' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://musor.tv/napi/tvmusor/HATOS_CSATORNA/2022.11.19') -}) - -it('can generate valid url for today', () => { - const today = dayjs.utc().startOf('d') - - expect(url({ channel, date: today })).toBe('https://musor.tv/mai/tvmusor/HATOS_CSATORNA') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-19T23:00:00.000Z', - stop: '2022-11-19T23:30:00.000Z', - title: 'Egészségtér', - description: - 'Egészségtér címmel új természetgyógyászattal foglalkozó magazinműsor indult hetente fél órás időtartamban a hatoscsatornán. A műsor derűs, objektív hangvételével és szakmailag magas színvonalú ismeretterjesztő jellegével az e' - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-19T23:30:00.000Z', - stop: '2022-11-20T00:00:00.000Z', - title: 'Tradíció Klipek', - description: 'Tradíció Klipek Birinyi József néprajzi, vallási, népzenei, népszokás filmjeiből.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/musor.tv/musor.tv.config.js --output=./sites/musor.tv/musor.tv.channels.xml +// npm run grab -- --site=musor.tv + +const { parser, url } = require('./musor.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'HATOS_CSATORNA', + xmltv_id: 'Hatoscsatorna.hu' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://musor.tv/napi/tvmusor/HATOS_CSATORNA/2022.11.19') +}) + +it('can generate valid url for today', () => { + const today = dayjs.utc().startOf('d') + + expect(url({ channel, date: today })).toBe('https://musor.tv/mai/tvmusor/HATOS_CSATORNA') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-19T23:00:00.000Z', + stop: '2022-11-19T23:30:00.000Z', + title: 'Egészségtér', + description: + 'Egészségtér címmel új természetgyógyászattal foglalkozó magazinműsor indult hetente fél órás időtartamban a hatoscsatornán. A műsor derűs, objektív hangvételével és szakmailag magas színvonalú ismeretterjesztő jellegével az e' + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-19T23:30:00.000Z', + stop: '2022-11-20T00:00:00.000Z', + title: 'Tradíció Klipek', + description: 'Tradíció Klipek Birinyi József néprajzi, vallási, népzenei, népszokás filmjeiből.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js b/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js index 0969598d8..d2de4c3f1 100644 --- a/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js +++ b/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js @@ -1,76 +1,76 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -module.exports = { - site: 'myafn.dodmedia.osd.mil', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `https://v3.myafn.dodmedia.osd.mil/api/json/32/${date.format( - 'YYYY-MM-DD' - )}@0000/${date.format('YYYY-MM-DD')}@2359/schedule.json` - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.o, 'm') - programs.push({ - title: item.h, - sub_title: item.i, - description: item.l, - rating: parseRating(item), - category: parseCategory(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://v3.myafn.dodmedia.osd.mil/api/json/32/channels.json') - .then(r => r.data) - .catch(console.log) - - return data.map(item => ({ - site_id: item.Channel, - name: item.Title - })) - } -} - -function parseStart(item) { - return dayjs.utc(item.e, 'YYYY,M,D,H,m,s,0').add(1, 'month') -} - -function parseCategory(item) { - return item.m ? item.m.split(',') : [] -} - -function parseRating(item) { - return item.j - ? { - system: 'MPA', - value: item.j - } - : null -} - -function parseItems(content, channel) { - const items = JSON.parse(content) - if (!Array.isArray(items)) return [] - - return items.filter(i => i.b == channel.site_id) -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +module.exports = { + site: 'myafn.dodmedia.osd.mil', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `https://v3.myafn.dodmedia.osd.mil/api/json/32/${date.format( + 'YYYY-MM-DD' + )}@0000/${date.format('YYYY-MM-DD')}@2359/schedule.json` + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.o, 'm') + programs.push({ + title: item.h, + sub_title: item.i, + description: item.l, + rating: parseRating(item), + category: parseCategory(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://v3.myafn.dodmedia.osd.mil/api/json/32/channels.json') + .then(r => r.data) + .catch(console.log) + + return data.map(item => ({ + site_id: item.Channel, + name: item.Title + })) + } +} + +function parseStart(item) { + return dayjs.utc(item.e, 'YYYY,M,D,H,m,s,0').add(1, 'month') +} + +function parseCategory(item) { + return item.m ? item.m.split(',') : [] +} + +function parseRating(item) { + return item.j + ? { + system: 'MPA', + value: item.j + } + : null +} + +function parseItems(content, channel) { + const items = JSON.parse(content) + if (!Array.isArray(items)) return [] + + return items.filter(i => i.b == channel.site_id) +} diff --git a/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.test.js b/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.test.js index 1fefb07c8..38e410b7c 100644 --- a/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.test.js +++ b/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.test.js @@ -1,58 +1,58 @@ -// npm run grab -- --site=myafn.dodmedia.osd.mil -// npm run channels:parse -- --config=./sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js --output=./sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.channels.xml - -const { parser, url } = require('./myafn.dodmedia.osd.mil.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'AFNPrimeAtlantic.us' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://v3.myafn.dodmedia.osd.mil/api/json/32/2022-10-03@0000/2022-10-03@2359/schedule.json' - ) -}) - -it('can parse response', () => { - const content = - '[{"a":566,"b":2,"c":"2022,9,3,3,0,0,0","d":"2022,9,3,4,0,0,0","e":"2022,9,3,3,0,0,0","f":"2022,9,3,4,0,0,0","g":60,"h":"This Week with George Stephanopoulos (ABC)","i":"Episode Title","j":"TV-14","k":false,"l":"Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show \\"\\"Good Morning America,\\"\\" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.","m":"News,Politics,Public affairs,Talk","n":694284445,"o":60,"p":20,"q":true,"r":694285705,"s":null}]' - const result = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-03T03:00:00.000Z', - stop: '2022-10-03T04:00:00.000Z', - title: 'This Week with George Stephanopoulos (ABC)', - sub_title: 'Episode Title', - description: - 'Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show ""Good Morning America,"" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.', - category: ['News', 'Politics', 'Public affairs', 'Talk'], - rating: { - system: 'MPA', - value: 'TV-14' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: `{ - "Message": "An error has occurred." - }`, - date, - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=myafn.dodmedia.osd.mil +// npm run channels:parse -- --config=./sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js --output=./sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.channels.xml + +const { parser, url } = require('./myafn.dodmedia.osd.mil.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'AFNPrimeAtlantic.us' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://v3.myafn.dodmedia.osd.mil/api/json/32/2022-10-03@0000/2022-10-03@2359/schedule.json' + ) +}) + +it('can parse response', () => { + const content = + '[{"a":566,"b":2,"c":"2022,9,3,3,0,0,0","d":"2022,9,3,4,0,0,0","e":"2022,9,3,3,0,0,0","f":"2022,9,3,4,0,0,0","g":60,"h":"This Week with George Stephanopoulos (ABC)","i":"Episode Title","j":"TV-14","k":false,"l":"Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show \\"\\"Good Morning America,\\"\\" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.","m":"News,Politics,Public affairs,Talk","n":694284445,"o":60,"p":20,"q":true,"r":694285705,"s":null}]' + const result = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-03T03:00:00.000Z', + stop: '2022-10-03T04:00:00.000Z', + title: 'This Week with George Stephanopoulos (ABC)', + sub_title: 'Episode Title', + description: + 'Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show ""Good Morning America,"" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.', + category: ['News', 'Politics', 'Public affairs', 'Talk'], + rating: { + system: 'MPA', + value: 'TV-14' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: `{ + "Message": "An error has occurred." + }`, + date, + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mysky.com.ph/mysky.com.ph.config.js b/sites/mysky.com.ph/mysky.com.ph.config.js index af604cb26..009dacab5 100644 --- a/sites/mysky.com.ph/mysky.com.ph.config.js +++ b/sites/mysky.com.ph/mysky.com.ph.config.js @@ -1,62 +1,62 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mysky.com.ph', - days: 2, - url: 'https://skyepg.mysky.com.ph/Main/getEventsbyType', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.userData.description, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://skyepg.mysky.com.ph/Main/getEventsbyType') - .then(r => r.data.location) - .catch(console.log) - - return items.map(item => ({ - site_id: item.id, - name: item.name - })) - } -} - -function parseStart(item) { - return dayjs.tz(item.start, 'YYYY/MM/DD HH:mm', 'Asia/Manila') -} - -function parseStop(item) { - return dayjs.tz(item.end, 'YYYY/MM/DD HH:mm', 'Asia/Manila') -} - -function parseItems(content, channel, date) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.events)) return [] - const d = date.format('YYYY/MM/DD') - - return data.events.filter(i => i.location == channel.site_id && i.start.includes(d)) -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mysky.com.ph', + days: 2, + url: 'https://skyepg.mysky.com.ph/Main/getEventsbyType', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.userData.description, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://skyepg.mysky.com.ph/Main/getEventsbyType') + .then(r => r.data.location) + .catch(console.log) + + return items.map(item => ({ + site_id: item.id, + name: item.name + })) + } +} + +function parseStart(item) { + return dayjs.tz(item.start, 'YYYY/MM/DD HH:mm', 'Asia/Manila') +} + +function parseStop(item) { + return dayjs.tz(item.end, 'YYYY/MM/DD HH:mm', 'Asia/Manila') +} + +function parseItems(content, channel, date) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.events)) return [] + const d = date.format('YYYY/MM/DD') + + return data.events.filter(i => i.location == channel.site_id && i.start.includes(d)) +} diff --git a/sites/mysky.com.ph/mysky.com.ph.test.js b/sites/mysky.com.ph/mysky.com.ph.test.js index b53c25ae4..71063c766 100644 --- a/sites/mysky.com.ph/mysky.com.ph.test.js +++ b/sites/mysky.com.ph/mysky.com.ph.test.js @@ -1,47 +1,47 @@ -// npm run grab -- --site=mysky.com.ph -// npm run channels:parse -- --config=./sites/mysky.com.ph/mysky.com.ph.config.js --output=./sites/mysky.com.ph/mysky.com.ph.channels.xml - -const { parser, url } = require('./mysky.com.ph.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '8', - xmltv_id: 'KapamilyaChannel.ph' -} - -it('can generate valid url', () => { - expect(url).toBe('https://skyepg.mysky.com.ph/Main/getEventsbyType') -}) - -it('can parse response', () => { - const content = - '{"events":[{"name":"TV PATROL","location":"8","start":"2022/10/04 19:00","end":"2022/10/04 20:00","userData":{"description":"Description example"}},{"name":"DARNA","location":"8","start":"2022/10/05 20:00","end":"2022/10/05 20:45","userData":{"description":""}},{"name":"Zoe Bakes S1","location":"22","start":"2022/10/04 20:30","end":"2022/10/04 21:00","userData":{"description":"Zo Franois Dad is a beekeeper. So for his birthday, she bakes him a special beehiveshaped cake."}}]}' - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-04T11:00:00.000Z', - stop: '2022-10-04T12:00:00.000Z', - title: 'TV PATROL', - description: 'Description example' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel, - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mysky.com.ph +// npm run channels:parse -- --config=./sites/mysky.com.ph/mysky.com.ph.config.js --output=./sites/mysky.com.ph/mysky.com.ph.channels.xml + +const { parser, url } = require('./mysky.com.ph.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '8', + xmltv_id: 'KapamilyaChannel.ph' +} + +it('can generate valid url', () => { + expect(url).toBe('https://skyepg.mysky.com.ph/Main/getEventsbyType') +}) + +it('can parse response', () => { + const content = + '{"events":[{"name":"TV PATROL","location":"8","start":"2022/10/04 19:00","end":"2022/10/04 20:00","userData":{"description":"Description example"}},{"name":"DARNA","location":"8","start":"2022/10/05 20:00","end":"2022/10/05 20:45","userData":{"description":""}},{"name":"Zoe Bakes S1","location":"22","start":"2022/10/04 20:30","end":"2022/10/04 21:00","userData":{"description":"Zo Franois Dad is a beekeeper. So for his birthday, she bakes him a special beehiveshaped cake."}}]}' + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-04T11:00:00.000Z', + stop: '2022-10-04T12:00:00.000Z', + title: 'TV PATROL', + description: 'Description example' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel, + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mytvsuper.com/mytvsuper.com.config.js b/sites/mytvsuper.com/mytvsuper.com.config.js index 8a73c0e98..7d3bb17fd 100644 --- a/sites/mytvsuper.com/mytvsuper.com.config.js +++ b/sites/mytvsuper.com/mytvsuper.com.config.js @@ -1,82 +1,82 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -const API_ENDPOINT = 'https://content-api.mytvsuper.com/v1' - -module.exports = { - site: 'mytvsuper.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/epg?network_code=${channel.site_id}&from=${date.format( - 'YYYYMMDD' - )}&to=${date.format('YYYYMMDD')}&platform=web` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, date) - for (let item of items) { - const prev = programs[programs.length - 1] - const start = parseStart(item) - const stop = start.add(30, 'm') - if (prev) { - prev.stop = start - } - programs.push({ - title: parseTitle(item, channel), - description: parseDescription(item, channel), - episode: parseInt(item.episode_no), - start: start, - stop: stop - }) - } - - return programs - }, - async channels({ lang }) { - const data = await axios - .get(`${API_ENDPOINT}/channel/list?platform=web`) - .then(r => r.data) - .catch(console.error) - - return data.channels.map(c => { - const name = lang === 'en' ? c.name_en : c.name_tc - - return { - site_id: c.network_code, - name, - lang - } - }) - } -} - -function parseTitle(item, channel) { - return channel.lang === 'en' ? item.programme_title_en : item.programme_title_tc -} - -function parseDescription(item, channel) { - return channel.lang === 'en' ? item.episode_synopsis_en : item.episode_synopsis_tc -} - -function parseStart(item) { - return dayjs.tz(item.start_datetime, 'Asia/Hong_Kong') -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!Array.isArray(data) || !data.length || !Array.isArray(data[0].item)) return [] - const dayData = data[0].item.find(i => i.date === date.format('YYYY-MM-DD')) - if (!dayData || !Array.isArray(dayData.epg)) return [] - - return dayData.epg -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +const API_ENDPOINT = 'https://content-api.mytvsuper.com/v1' + +module.exports = { + site: 'mytvsuper.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/epg?network_code=${channel.site_id}&from=${date.format( + 'YYYYMMDD' + )}&to=${date.format('YYYYMMDD')}&platform=web` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, date) + for (let item of items) { + const prev = programs[programs.length - 1] + const start = parseStart(item) + const stop = start.add(30, 'm') + if (prev) { + prev.stop = start + } + programs.push({ + title: parseTitle(item, channel), + description: parseDescription(item, channel), + episode: parseInt(item.episode_no), + start: start, + stop: stop + }) + } + + return programs + }, + async channels({ lang }) { + const data = await axios + .get(`${API_ENDPOINT}/channel/list?platform=web`) + .then(r => r.data) + .catch(console.error) + + return data.channels.map(c => { + const name = lang === 'en' ? c.name_en : c.name_tc + + return { + site_id: c.network_code, + name, + lang + } + }) + } +} + +function parseTitle(item, channel) { + return channel.lang === 'en' ? item.programme_title_en : item.programme_title_tc +} + +function parseDescription(item, channel) { + return channel.lang === 'en' ? item.episode_synopsis_en : item.episode_synopsis_tc +} + +function parseStart(item) { + return dayjs.tz(item.start_datetime, 'Asia/Hong_Kong') +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!Array.isArray(data) || !data.length || !Array.isArray(data[0].item)) return [] + const dayData = data[0].item.find(i => i.date === date.format('YYYY-MM-DD')) + if (!dayData || !Array.isArray(dayData.epg)) return [] + + return dayData.epg +} diff --git a/sites/mytvsuper.com/mytvsuper.com.test.js b/sites/mytvsuper.com/mytvsuper.com.test.js index 6025d4922..11b66bf3d 100644 --- a/sites/mytvsuper.com/mytvsuper.com.test.js +++ b/sites/mytvsuper.com/mytvsuper.com.test.js @@ -1,71 +1,71 @@ -// npm run channels:parse -- --config=./sites/mytvsuper.com/mytvsuper.com.config.js --output=./sites/mytvsuper.com/mytvsuper.com.channels.xml --set=lang:zh -// npm run grab -- --site=mytvsuper.com - -const { parser, url } = require('./mytvsuper.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'B', - xmltv_id: 'J2.hk', - lang: 'zh' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://content-api.mytvsuper.com/v1/epg?network_code=B&from=20221115&to=20221115&platform=web' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: '想見你#3[粵/普][PG]', - description: - '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。', - episode: 1000003 - }) -}) - -it('can parse response in English', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const channelEN = { ...channel, lang: 'en' } - let results = parser({ content, channel: channelEN, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: 'Someday or One Day#3[Can/Man][PG]', - description: 'Description', - episode: 1000003 - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ date, channel, content }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/mytvsuper.com/mytvsuper.com.config.js --output=./sites/mytvsuper.com/mytvsuper.com.channels.xml --set=lang:zh +// npm run grab -- --site=mytvsuper.com + +const { parser, url } = require('./mytvsuper.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'B', + xmltv_id: 'J2.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://content-api.mytvsuper.com/v1/epg?network_code=B&from=20221115&to=20221115&platform=web' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: '想見你#3[粵/普][PG]', + description: + '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。', + episode: 1000003 + }) +}) + +it('can parse response in English', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const channelEN = { ...channel, lang: 'en' } + let results = parser({ content, channel: channelEN, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: 'Someday or One Day#3[Can/Man][PG]', + description: 'Description', + episode: 1000003 + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ date, channel, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nhk.or.jp/nhk.or.jp.config.js b/sites/nhk.or.jp/nhk.or.jp.config.js index b43bf6a46..a8ed9e351 100644 --- a/sites/nhk.or.jp/nhk.or.jp.config.js +++ b/sites/nhk.or.jp/nhk.or.jp.config.js @@ -1,70 +1,70 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'nhk.or.jp', - days: 5, - output: 'nhk.or.jp.guide.xml', - channels: 'nhk.or.jp.channels.xml', - lang: 'en', - delay: 5000, - - url: function ({ date }) { - return `https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s${date.unix() * 1000}-e${ - date.add(1, 'd').unix() * 1000 - }.json` - }, - - request: { - method: 'GET', - timeout: 5000, - cache: { ttl: 60 * 1000 }, - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - } - }, - - logo: function (context) { - return context.channel.logo - }, - - parser: function (context) { - const programs = [] - - const items = parseItems(context.content) - - items.forEach(item => { - programs.push({ - title: item.title, - start: parseStart(item), - stop: parseStop(item), - description: item.description, - icon: parseIcon(item), - sub_title: item.subtitle - }) - }) - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.channel || !Array.isArray(data.channel.item) ? [] : data.channel.item - } else { - return [] - } -} - -function parseStart(item) { - return dayjs.unix(parseInt(item.pubDate) / 1000) -} - -function parseStop(item) { - return dayjs.unix(parseInt(item.endDate) / 1000) -} - -function parseIcon(item) { - return 'https://www.nhk.or.jp' + item.thumbnail -} +const dayjs = require('dayjs') + +module.exports = { + site: 'nhk.or.jp', + days: 5, + output: 'nhk.or.jp.guide.xml', + channels: 'nhk.or.jp.channels.xml', + lang: 'en', + delay: 5000, + + url: function ({ date }) { + return `https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s${date.unix() * 1000}-e${ + date.add(1, 'd').unix() * 1000 + }.json` + }, + + request: { + method: 'GET', + timeout: 5000, + cache: { ttl: 60 * 1000 }, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + } + }, + + logo: function (context) { + return context.channel.logo + }, + + parser: function (context) { + const programs = [] + + const items = parseItems(context.content) + + items.forEach(item => { + programs.push({ + title: item.title, + start: parseStart(item), + stop: parseStop(item), + description: item.description, + icon: parseIcon(item), + sub_title: item.subtitle + }) + }) + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.channel || !Array.isArray(data.channel.item) ? [] : data.channel.item + } else { + return [] + } +} + +function parseStart(item) { + return dayjs.unix(parseInt(item.pubDate) / 1000) +} + +function parseStop(item) { + return dayjs.unix(parseInt(item.endDate) / 1000) +} + +function parseIcon(item) { + return 'https://www.nhk.or.jp' + item.thumbnail +} diff --git a/sites/nhk.or.jp/nhk.or.jp.test.js b/sites/nhk.or.jp/nhk.or.jp.test.js index 66d35b0c0..b8512c1e9 100644 --- a/sites/nhk.or.jp/nhk.or.jp.test.js +++ b/sites/nhk.or.jp/nhk.or.jp.test.js @@ -1,45 +1,45 @@ -// npm run grab -- --site=nhk.or.jp -// npx jest nhk.or.jp.test.js - -const { url, parser } = require('./nhk.or.jp.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-04-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0', - xmltv_id: 'NHKWorldJapan.jp', - lang: 'en', - logo: 'https://www3.nhk.or.jp/nhkworld/common/site_images/nw_webapp_1024x1024.png' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s1682726400000-e1682812800000.json' - ) -}) - -it('can parse response', () => { - const content = - '{"channel":{"item":[{"seriesId":"1007","airingId":"000","title":"NHK NEWSLINE","description":"NHK WORLD-JAPAN\'s flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.","link":"/nhkworld/en/news/","pubDate":"1682726400000","endDate":"1682727000000","vodReserved":false,"jstrm":"1","wstrm":"1","subtitle":"","content":"","content_clean":"","pgm_gr_id":"","thumbnail":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg","thumbnail_s":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_small.jpg","showlist":"0","internal":"0","genre":{"TV":"11","Top":"","LC":""},"vod_id":"","vod_url":"","analytics":"[nhkworld]simul;NHK NEWSLINE;w02,001;1007-000-2023;2023-04-29T09:00:00+09:00"}]}}' - const results = parser({ content }) - - expect(results).toMatchObject([ - { - title: 'NHK NEWSLINE', - start: dayjs(1682726400000), - stop: dayjs(1682727000000), - description: - "NHK WORLD-JAPAN's flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.", - icon: 'https://www.nhk.or.jp/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg', - sub_title: '' - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=nhk.or.jp +// npx jest nhk.or.jp.test.js + +const { url, parser } = require('./nhk.or.jp.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-04-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0', + xmltv_id: 'NHKWorldJapan.jp', + lang: 'en', + logo: 'https://www3.nhk.or.jp/nhkworld/common/site_images/nw_webapp_1024x1024.png' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s1682726400000-e1682812800000.json' + ) +}) + +it('can parse response', () => { + const content = + '{"channel":{"item":[{"seriesId":"1007","airingId":"000","title":"NHK NEWSLINE","description":"NHK WORLD-JAPAN\'s flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.","link":"/nhkworld/en/news/","pubDate":"1682726400000","endDate":"1682727000000","vodReserved":false,"jstrm":"1","wstrm":"1","subtitle":"","content":"","content_clean":"","pgm_gr_id":"","thumbnail":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg","thumbnail_s":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_small.jpg","showlist":"0","internal":"0","genre":{"TV":"11","Top":"","LC":""},"vod_id":"","vod_url":"","analytics":"[nhkworld]simul;NHK NEWSLINE;w02,001;1007-000-2023;2023-04-29T09:00:00+09:00"}]}}' + const results = parser({ content }) + + expect(results).toMatchObject([ + { + title: 'NHK NEWSLINE', + start: dayjs(1682726400000), + stop: dayjs(1682727000000), + description: + "NHK WORLD-JAPAN's flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.", + icon: 'https://www.nhk.or.jp/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg', + sub_title: '' + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nhkworldpremium.com/nhkworldpremium.com.config.js b/sites/nhkworldpremium.com/nhkworldpremium.com.config.js index 7d64a51df..bdce0c268 100644 --- a/sites/nhkworldpremium.com/nhkworldpremium.com.config.js +++ b/sites/nhkworldpremium.com/nhkworldpremium.com.config.js @@ -1,56 +1,56 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'nhkworldpremium.com', - days: 7, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ channel }) { - return `https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=${channel.lang}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const start = dayjs.tz(item.schedule, 'Asia/Seoul') - const duration = parseDuration(item) - const stop = start.add(duration, 's') - programs.push({ - title: item.programTitle, - sub_title: item.episodeTitle, - start, - stop - }) - }) - - return programs - } -} - -function parseDuration(item) { - const [h, m, s] = item.period.split(':') - - if (!h || !m || !s) return 0 - - return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s) -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - - if (!data || !data.item || !Array.isArray(data.item.episodes)) return [] - - return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD'))) - } catch (err) { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'nhkworldpremium.com', + days: 7, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ channel }) { + return `https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=${channel.lang}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const start = dayjs.tz(item.schedule, 'Asia/Seoul') + const duration = parseDuration(item) + const stop = start.add(duration, 's') + programs.push({ + title: item.programTitle, + sub_title: item.episodeTitle, + start, + stop + }) + }) + + return programs + } +} + +function parseDuration(item) { + const [h, m, s] = item.period.split(':') + + if (!h || !m || !s) return 0 + + return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s) +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + + if (!data || !data.item || !Array.isArray(data.item.episodes)) return [] + + return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD'))) + } catch (err) { + return [] + } +} diff --git a/sites/nhkworldpremium.com/nhkworldpremium.com.test.js b/sites/nhkworldpremium.com/nhkworldpremium.com.test.js index 9b0978008..b1a6bf674 100644 --- a/sites/nhkworldpremium.com/nhkworldpremium.com.test.js +++ b/sites/nhkworldpremium.com/nhkworldpremium.com.test.js @@ -1,90 +1,90 @@ -// npm run grab -- --site=nhkworldpremium.com -// npm run grab -- --site=nhkworldpremium.com - -const { parser, url } = require('./nhkworldpremium.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-07-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'NHKWorldPremium.jp', - lang: 'en' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=en') -}) - -it('can generate valid url for Japanese guide', () => { - const channel = { - site_id: '#', - xmltv_id: 'NHKWorldPremium.jp', - lang: 'ja' - } - - expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=ja') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(56) - expect(results[0]).toMatchObject({ - start: '2023-07-09T15:35:00.000Z', - stop: '2023-07-09T16:20:00.000Z', - title: 'NHK Amateur Singing Contest', - sub_title: '"Maizuru City, Kyoto Prefecture"' - }) - - expect(results[55]).toMatchObject({ - start: '2023-07-10T14:35:00.000Z', - stop: '2023-07-10T15:15:00.000Z', - title: 'International News Report 2023', - sub_title: null - }) -}) - -it('can parse response with Japanese guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ja.json')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(56) - expect(results[0]).toMatchObject({ - start: '2023-07-09T15:35:00.000Z', - stop: '2023-07-09T16:20:00.000Z', - title: 'NHKのど自慢', - sub_title: '【京都から生放送!▽前川清・相川七瀬】' - }) - - expect(results[55]).toMatchObject({ - start: '2023-07-10T14:35:00.000Z', - stop: '2023-07-10T15:15:00.000Z', - title: '国際報道2023', - sub_title: null - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: {}, date }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=nhkworldpremium.com +// npm run grab -- --site=nhkworldpremium.com + +const { parser, url } = require('./nhkworldpremium.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-07-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'NHKWorldPremium.jp', + lang: 'en' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=en') +}) + +it('can generate valid url for Japanese guide', () => { + const channel = { + site_id: '#', + xmltv_id: 'NHKWorldPremium.jp', + lang: 'ja' + } + + expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=ja') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(56) + expect(results[0]).toMatchObject({ + start: '2023-07-09T15:35:00.000Z', + stop: '2023-07-09T16:20:00.000Z', + title: 'NHK Amateur Singing Contest', + sub_title: '"Maizuru City, Kyoto Prefecture"' + }) + + expect(results[55]).toMatchObject({ + start: '2023-07-10T14:35:00.000Z', + stop: '2023-07-10T15:15:00.000Z', + title: 'International News Report 2023', + sub_title: null + }) +}) + +it('can parse response with Japanese guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ja.json')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(56) + expect(results[0]).toMatchObject({ + start: '2023-07-09T15:35:00.000Z', + stop: '2023-07-09T16:20:00.000Z', + title: 'NHKのど自慢', + sub_title: '【京都から生放送!▽前川清・相川七瀬】' + }) + + expect(results[55]).toMatchObject({ + start: '2023-07-10T14:35:00.000Z', + stop: '2023-07-10T15:15:00.000Z', + title: '国際報道2023', + sub_title: null + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: {}, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nos.pt/nos.pt.config.js b/sites/nos.pt/nos.pt.config.js index 86c2fc389..14a5a902d 100644 --- a/sites/nos.pt/nos.pt.config.js +++ b/sites/nos.pt/nos.pt.config.js @@ -1,114 +1,114 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'nos.pt', - days: 2, - url({ channel }) { - return `https://www.nos.pt/particulares/televisao/guia-tv/Pages/channel.aspx?channel=${channel.site_id}` - }, - async parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - date = date.subtract(1, 'd') - for (let item of items) { - const $item = cheerio.load(item) - - const channelAcronym = parseChannelAcronym(content) - const programId = parseProgramId($item) - const details = await loadProgramDetails(channelAcronym, programId) - - programs.push({ - title: details.title, - description: details.description, - icon: parseIcon(details), - start: dayjs(details.start), - stop: dayjs(details.stop) - }) - } - - return programs - }, - async channels() { - const html = await axios - .get('https://www.nos.pt/particulares/televisao/guia-tv/Pages/default.aspx') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const items = $('#guide-filters > dl.dropdown-ord > dd > ul > li').toArray() - - return items.map(item => { - const $item = cheerio.load(item) - - return { - lang: 'pt', - site_id: $item('.value').text().trim(), - name: $item('a').clone().children().remove().end().text().trim() - } - }) - } -} - -async function loadProgramDetails(channelAcronym, programId) { - if (!channelAcronym || !programId) return {} - const data = await axios - .post( - 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails', - { - programId, - channelAcronym, - hour: 'undefined', - startHour: 'undefined', - endHour: 'undefined' - }, - { - headers: { - 'content-type': 'application/json; charset=UTF-8' - } - } - ) - .then(r => r.data) - .catch(console.log) - - if (!data) return {} - - const [title, description, image, , , , start, stop] = data.d.split('_#|$_') - - return { - title, - description, - image, - start, - stop - } -} - -function parseIcon(details) { - return details.image ? `https://images.nos.pt/${details.image}` : null -} - -function parseProgramId($item) { - return $item('a').attr('id') -} - -function parseChannelAcronym(content) { - const $ = cheerio.load(content) - - return $('#channel-logo > img').attr('alt') -} - -function parseItems(content, date) { - const day = date.date() - const $ = cheerio.load(content) - - return $(`#day${day} > ul > li`).toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'nos.pt', + days: 2, + url({ channel }) { + return `https://www.nos.pt/particulares/televisao/guia-tv/Pages/channel.aspx?channel=${channel.site_id}` + }, + async parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + date = date.subtract(1, 'd') + for (let item of items) { + const $item = cheerio.load(item) + + const channelAcronym = parseChannelAcronym(content) + const programId = parseProgramId($item) + const details = await loadProgramDetails(channelAcronym, programId) + + programs.push({ + title: details.title, + description: details.description, + icon: parseIcon(details), + start: dayjs(details.start), + stop: dayjs(details.stop) + }) + } + + return programs + }, + async channels() { + const html = await axios + .get('https://www.nos.pt/particulares/televisao/guia-tv/Pages/default.aspx') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const items = $('#guide-filters > dl.dropdown-ord > dd > ul > li').toArray() + + return items.map(item => { + const $item = cheerio.load(item) + + return { + lang: 'pt', + site_id: $item('.value').text().trim(), + name: $item('a').clone().children().remove().end().text().trim() + } + }) + } +} + +async function loadProgramDetails(channelAcronym, programId) { + if (!channelAcronym || !programId) return {} + const data = await axios + .post( + 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails', + { + programId, + channelAcronym, + hour: 'undefined', + startHour: 'undefined', + endHour: 'undefined' + }, + { + headers: { + 'content-type': 'application/json; charset=UTF-8' + } + } + ) + .then(r => r.data) + .catch(console.log) + + if (!data) return {} + + const [title, description, image, , , , start, stop] = data.d.split('_#|$_') + + return { + title, + description, + image, + start, + stop + } +} + +function parseIcon(details) { + return details.image ? `https://images.nos.pt/${details.image}` : null +} + +function parseProgramId($item) { + return $item('a').attr('id') +} + +function parseChannelAcronym(content) { + const $ = cheerio.load(content) + + return $('#channel-logo > img').attr('alt') +} + +function parseItems(content, date) { + const day = date.date() + const $ = cheerio.load(content) + + return $(`#day${day} > ul > li`).toArray() +} diff --git a/sites/nos.pt/nos.pt.test.js b/sites/nos.pt/nos.pt.test.js index 7ff15af12..3398b08ea 100644 --- a/sites/nos.pt/nos.pt.test.js +++ b/sites/nos.pt/nos.pt.test.js @@ -1,100 +1,100 @@ -// npm run channels:parse -- --config=./sites/nos.pt/nos.pt.config.js --output=./sites/nos.pt/nos.pt.channels.xml -// npm run grab -- --site=nos.pt - -const { parser, url } = require('./nos.pt.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5', - xmltv_id: 'RTP1.pt' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe( - 'https://www.nos.pt/particulares/televisao/guia-tv/Pages/channel.aspx?channel=5' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - - axios.post.mockImplementation((url, data) => { - if ( - url === - 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails' && - JSON.stringify(data) === - JSON.stringify({ - programId: '81361', - channelAcronym: 'RTP1', - hour: 'undefined', - startHour: 'undefined', - endHour: 'undefined' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_0.json'))) - }) - } else if ( - url === - 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails' && - JSON.stringify(data) === - JSON.stringify({ - programId: '81382', - channelAcronym: 'RTP1', - hour: 'undefined', - startHour: 'undefined', - endHour: 'undefined' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_21.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-27T23:50:00.000Z', - stop: '2023-01-28T00:36:00.000Z', - title: 'Anatomia de Grey T.17 Ep.3', - description: - 'Os médicos do Grey Sloan continuam a enfrentar a nova realidade do COVID-19 e lidam com um paciente conhecido e teimoso. Koracick fica encarregue dos internos e Link opera um terapeuta sexual.', - icon: 'https://images.nos.pt/b6fd27f4bd0b404abd4c3fc4faa79024_resized_352x198.jpg' - }) - - expect(results[21]).toMatchObject({ - start: '2023-01-28T21:38:00.000Z', - stop: '2023-01-29T00:05:00.000Z', - title: 'MasterChef Portugal T.1 Ep.10', - description: - 'A maior competição de cozinha do mundo arranca ao comando de três dos mais conceituados chefs portugueses: Pedro Pena Bastos, Noélia Jerónimo e Ricardo Costa, que nos vão transmitir os seus conhecimentos e a sua paixão pela cozinha.', - icon: 'https://images.nos.pt/8aa511d697f0401a88a0cb1ec2718cc3_resized_352x198.jpg' - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/nos.pt/nos.pt.config.js --output=./sites/nos.pt/nos.pt.channels.xml +// npm run grab -- --site=nos.pt + +const { parser, url } = require('./nos.pt.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5', + xmltv_id: 'RTP1.pt' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://www.nos.pt/particulares/televisao/guia-tv/Pages/channel.aspx?channel=5' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + + axios.post.mockImplementation((url, data) => { + if ( + url === + 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails' && + JSON.stringify(data) === + JSON.stringify({ + programId: '81361', + channelAcronym: 'RTP1', + hour: 'undefined', + startHour: 'undefined', + endHour: 'undefined' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_0.json'))) + }) + } else if ( + url === + 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails' && + JSON.stringify(data) === + JSON.stringify({ + programId: '81382', + channelAcronym: 'RTP1', + hour: 'undefined', + startHour: 'undefined', + endHour: 'undefined' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_21.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-27T23:50:00.000Z', + stop: '2023-01-28T00:36:00.000Z', + title: 'Anatomia de Grey T.17 Ep.3', + description: + 'Os médicos do Grey Sloan continuam a enfrentar a nova realidade do COVID-19 e lidam com um paciente conhecido e teimoso. Koracick fica encarregue dos internos e Link opera um terapeuta sexual.', + icon: 'https://images.nos.pt/b6fd27f4bd0b404abd4c3fc4faa79024_resized_352x198.jpg' + }) + + expect(results[21]).toMatchObject({ + start: '2023-01-28T21:38:00.000Z', + stop: '2023-01-29T00:05:00.000Z', + title: 'MasterChef Portugal T.1 Ep.10', + description: + 'A maior competição de cozinha do mundo arranca ao comando de três dos mais conceituados chefs portugueses: Pedro Pena Bastos, Noélia Jerónimo e Ricardo Costa, que nos vão transmitir os seus conhecimentos e a sua paixão pela cozinha.', + icon: 'https://images.nos.pt/8aa511d697f0401a88a0cb1ec2718cc3_resized_352x198.jpg' + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/novacyprus.com/novacyprus.com.config.js b/sites/novacyprus.com/novacyprus.com.config.js index 5096014db..0eb06083e 100644 --- a/sites/novacyprus.com/novacyprus.com.config.js +++ b/sites/novacyprus.com/novacyprus.com.config.js @@ -1,67 +1,67 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 - -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'novacyprus.com', - days: 2, - url({ date }) { - return `https://www.novacyprus.com/api/v1/tvprogram/from/${date.format('YYYYMMDD')}/to/${date - .add(1, 'd') - .format('YYYYMMDD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(item.slotDuration, 'm') - programs.push({ - title: item.title, - description: item.description, - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://www.novacyprus.com/api/v1/guide/dailychannels') - .then(r => r.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'el', - site_id: item.ChannelId, - name: item.nameEl - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.datetime, 'YYYY-MM-DD HH:mm:ss', 'Asia/Nicosia') -} - -function parseIcon(item) { - return item.mediaItems.length ? item.mediaItems[0].CdnUrl : null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.nodes)) return [] - - return data.nodes.filter(i => i.ChannelId === channel.site_id) -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 + +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'novacyprus.com', + days: 2, + url({ date }) { + return `https://www.novacyprus.com/api/v1/tvprogram/from/${date.format('YYYYMMDD')}/to/${date + .add(1, 'd') + .format('YYYYMMDD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(item.slotDuration, 'm') + programs.push({ + title: item.title, + description: item.description, + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://www.novacyprus.com/api/v1/guide/dailychannels') + .then(r => r.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'el', + site_id: item.ChannelId, + name: item.nameEl + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.datetime, 'YYYY-MM-DD HH:mm:ss', 'Asia/Nicosia') +} + +function parseIcon(item) { + return item.mediaItems.length ? item.mediaItems[0].CdnUrl : null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.nodes)) return [] + + return data.nodes.filter(i => i.ChannelId === channel.site_id) +} diff --git a/sites/novacyprus.com/novacyprus.com.test.js b/sites/novacyprus.com/novacyprus.com.test.js index 282ce1e3c..1e7feee58 100644 --- a/sites/novacyprus.com/novacyprus.com.test.js +++ b/sites/novacyprus.com/novacyprus.com.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/novacyprus.com/novacyprus.com.config.js --output=./sites/novacyprus.com/novacyprus.com.channels.xml -// npm run grab -- --site=novacyprus.com - -const { parser, url } = require('./novacyprus.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '614', - xmltv_id: 'NovaCinema1.gr' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.novacyprus.com/api/v1/tvprogram/from/20211117/to/20211118' - ) -}) - -it('can parse response', () => { - const content = - '{"nodes":[{"datetime":"2021-11-17 06:20:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema1HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000762/d6a2f5e0-dbc0-49c7-9843-e3161ca5ae5d.png","cid":"42","ChannelId":"614","startingTime":"06:20","endTime":"08:10","title":"Δεσμοί Αίματος","description":"Θρίλερ Μυστηρίου","duration":"109","slotDuration":"110","bref":"COMMOBLOOX","mediaItems":[{"MediaListTypeId":"6","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg"},{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_BACKGROUND_CYP.jpg"}]},{"datetime":"2021-11-17 06:00:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema2HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000763/24e05354-d6ad-4949-bcb3-a81d1c1d2cba.png","cid":"62","ChannelId":"653","startingTime":"06:00","endTime":"07:40","title":"Ανυπόφοροι Γείτονες","description":"Κωμωδία","duration":"93","slotDuration":"100","bref":"NEIGHBORSX","mediaItems":[{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_BACKGROUND_CYP.jpg"}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-17T04:20:00.000Z', - stop: '2021-11-17T06:10:00.000Z', - title: 'Δεσμοί Αίματος', - description: 'Θρίλερ Μυστηρίου', - icon: 'http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"nodes":[],"total":0,"pages":0}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/novacyprus.com/novacyprus.com.config.js --output=./sites/novacyprus.com/novacyprus.com.channels.xml +// npm run grab -- --site=novacyprus.com + +const { parser, url } = require('./novacyprus.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '614', + xmltv_id: 'NovaCinema1.gr' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.novacyprus.com/api/v1/tvprogram/from/20211117/to/20211118' + ) +}) + +it('can parse response', () => { + const content = + '{"nodes":[{"datetime":"2021-11-17 06:20:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema1HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000762/d6a2f5e0-dbc0-49c7-9843-e3161ca5ae5d.png","cid":"42","ChannelId":"614","startingTime":"06:20","endTime":"08:10","title":"Δεσμοί Αίματος","description":"Θρίλερ Μυστηρίου","duration":"109","slotDuration":"110","bref":"COMMOBLOOX","mediaItems":[{"MediaListTypeId":"6","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg"},{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_BACKGROUND_CYP.jpg"}]},{"datetime":"2021-11-17 06:00:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema2HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000763/24e05354-d6ad-4949-bcb3-a81d1c1d2cba.png","cid":"62","ChannelId":"653","startingTime":"06:00","endTime":"07:40","title":"Ανυπόφοροι Γείτονες","description":"Κωμωδία","duration":"93","slotDuration":"100","bref":"NEIGHBORSX","mediaItems":[{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_BACKGROUND_CYP.jpg"}]}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-17T04:20:00.000Z', + stop: '2021-11-17T06:10:00.000Z', + title: 'Δεσμοί Αίματος', + description: 'Θρίλερ Μυστηρίου', + icon: 'http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"nodes":[],"total":0,"pages":0}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/novasports.gr/novasports.gr.config.js b/sites/novasports.gr/novasports.gr.config.js index 0721f670e..5e452c579 100644 --- a/sites/novasports.gr/novasports.gr.config.js +++ b/sites/novasports.gr/novasports.gr.config.js @@ -1,87 +1,87 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'novasports.gr', - days: 2, - url: function ({ date }) { - return `https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - let stop = start.add(30, 'm') - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - stop = stop.add(1, 'd') - } - prev.stop = start - } - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get( - 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' - ) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $( - '#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div' - ).toArray() - return items.map(item => { - const name = $(item).find('.channel').text().trim() - - return { - lang: 'el', - site_id: name, - name - } - }) - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseDescription($item) { - return $item('.subtitle').text().trim() -} - -function parseStart($item, date) { - const timeString = $item('.time').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${timeString}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - const $channelElement = $( - `#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div:contains("${channel.site_id}")` - ) - - return $channelElement.find('.channel-program > div').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'novasports.gr', + days: 2, + url: function ({ date }) { + return `https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + let stop = start.add(30, 'm') + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + stop = stop.add(1, 'd') + } + prev.stop = start + } + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get( + 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' + ) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $( + '#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div' + ).toArray() + return items.map(item => { + const name = $(item).find('.channel').text().trim() + + return { + lang: 'el', + site_id: name, + name + } + }) + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseDescription($item) { + return $item('.subtitle').text().trim() +} + +function parseStart($item, date) { + const timeString = $item('.time').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${timeString}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + const $channelElement = $( + `#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div:contains("${channel.site_id}")` + ) + + return $channelElement.find('.channel-program > div').toArray() +} diff --git a/sites/novasports.gr/novasports.gr.test.js b/sites/novasports.gr/novasports.gr.test.js index 45f71cc88..0a2c33880 100644 --- a/sites/novasports.gr/novasports.gr.test.js +++ b/sites/novasports.gr/novasports.gr.test.js @@ -1,49 +1,49 @@ -// npm run channels:parse -- --config=./sites/novasports.gr/novasports.gr.config.js --output=./sites/novasports.gr/novasports.gr.channels.xml -// npm run grab -- --site=novasports.gr - -const { parser, url } = require('./novasports.gr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Novasports Premier League', - xmltv_id: 'NovasportsPremierLeague.gr' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T07:00:00.000Z', - stop: '2022-10-29T07:30:00.000Z', - title: 'Classic Match', - description: 'Τσέλσι - Μάντσεστερ Γ. (1999/00)' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - channel, - date - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/novasports.gr/novasports.gr.config.js --output=./sites/novasports.gr/novasports.gr.channels.xml +// npm run grab -- --site=novasports.gr + +const { parser, url } = require('./novasports.gr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Novasports Premier League', + xmltv_id: 'NovasportsPremierLeague.gr' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T07:00:00.000Z', + stop: '2022-10-29T07:30:00.000Z', + title: 'Classic Match', + description: 'Τσέλσι - Μάντσεστερ Γ. (1999/00)' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + channel, + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nowplayer.now.com/nowplayer.now.com.config.js b/sites/nowplayer.now.com/nowplayer.now.com.config.js index 9cf554ed6..840803456 100644 --- a/sites/nowplayer.now.com/nowplayer.now.com.config.js +++ b/sites/nowplayer.now.com/nowplayer.now.com.config.js @@ -1,69 +1,69 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'nowplayer.now.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') + 1 - - return `https://nowplayer.now.com/tvguide/epglist?channelIdList[]=${channel.site_id}&day=${diff}` - }, - request: { - headers({ channel }) { - return { - Cookie: `LANG=${channel.lang}; Expires=null; Path=/; Domain=nowplayer.now.com` - } - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels({ lang }) { - const html = await axios - .get('https://nowplayer.now.com/channels', { headers: { Accept: 'text/html' } }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const channels = $('body > div.container > .tv-guide-s-g > div > div').toArray() - - return channels.map(item => { - const $item = cheerio.load(item) - return { - lang, - site_id: $item('.guide-g-play > p.channel').text().replace('CH', ''), - name: $item('.thumbnail > a > span.image > p').text() - } - }) - } -} - -function parseStart(item) { - return dayjs(item.start) -} - -function parseStop(item) { - return dayjs(item.end) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data)) return [] - - return Array.isArray(data[0]) ? data[0] : [] -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'nowplayer.now.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + 1 + + return `https://nowplayer.now.com/tvguide/epglist?channelIdList[]=${channel.site_id}&day=${diff}` + }, + request: { + headers({ channel }) { + return { + Cookie: `LANG=${channel.lang}; Expires=null; Path=/; Domain=nowplayer.now.com` + } + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels({ lang }) { + const html = await axios + .get('https://nowplayer.now.com/channels', { headers: { Accept: 'text/html' } }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const channels = $('body > div.container > .tv-guide-s-g > div > div').toArray() + + return channels.map(item => { + const $item = cheerio.load(item) + return { + lang, + site_id: $item('.guide-g-play > p.channel').text().replace('CH', ''), + name: $item('.thumbnail > a > span.image > p').text() + } + }) + } +} + +function parseStart(item) { + return dayjs(item.start) +} + +function parseStop(item) { + return dayjs(item.end) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data)) return [] + + return Array.isArray(data[0]) ? data[0] : [] +} diff --git a/sites/nowplayer.now.com/nowplayer.now.com.test.js b/sites/nowplayer.now.com/nowplayer.now.com.test.js index da866de9e..6f26dbe7e 100644 --- a/sites/nowplayer.now.com/nowplayer.now.com.test.js +++ b/sites/nowplayer.now.com/nowplayer.now.com.test.js @@ -1,60 +1,60 @@ -// npm run channels:parse -- --config=./sites/nowplayer.now.com/nowplayer.now.com.config.js --output=./sites/nowplayer.now.com/nowplayer.now.com.channels.xml --set=lang:zh -// npm run grab -- --site=nowplayer.now.com - -const { parser, url, request } = require('./nowplayer.now.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const channel = { - lang: 'zh', - site_id: '096', - xmltv_id: 'ViuTVsix.hk' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=1' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=2' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - Cookie: 'LANG=zh; Expires=null; Path=/; Domain=nowplayer.now.com' - }) -}) - -it('can parse response', () => { - const content = - '[[{"key":"key_202111174524739","vimProgramId":"202111174524739","name":"ViuTVsix Station Closing","start":1637690400000,"end":1637715600000,"date":"20211124","startTime":"02:00AM","endTime":"09:00AM","duration":420,"recordable":false,"restartTv":false,"npvrProg":false,"npvrStartTime":0,"npvrEndTime":0,"cid":"viutvsix station closing","cc":"","isInWatchlist":false}]]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-23T18:00:00.000Z', - stop: '2021-11-24T01:00:00.000Z', - title: 'ViuTVsix Station Closing' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[[]]' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/nowplayer.now.com/nowplayer.now.com.config.js --output=./sites/nowplayer.now.com/nowplayer.now.com.channels.xml --set=lang:zh +// npm run grab -- --site=nowplayer.now.com + +const { parser, url, request } = require('./nowplayer.now.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const channel = { + lang: 'zh', + site_id: '096', + xmltv_id: 'ViuTVsix.hk' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=1' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=2' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + Cookie: 'LANG=zh; Expires=null; Path=/; Domain=nowplayer.now.com' + }) +}) + +it('can parse response', () => { + const content = + '[[{"key":"key_202111174524739","vimProgramId":"202111174524739","name":"ViuTVsix Station Closing","start":1637690400000,"end":1637715600000,"date":"20211124","startTime":"02:00AM","endTime":"09:00AM","duration":420,"recordable":false,"restartTv":false,"npvrProg":false,"npvrStartTime":0,"npvrEndTime":0,"cid":"viutvsix station closing","cc":"","isInWatchlist":false}]]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-23T18:00:00.000Z', + stop: '2021-11-24T01:00:00.000Z', + title: 'ViuTVsix Station Closing' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[[]]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js index c337095d3..4d002bf30 100644 --- a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js +++ b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js @@ -1,106 +1,106 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.nuevosiglo.com.uy/programacion/getGrilla' - -module.exports = { - site: 'nuevosiglo.com.uy', - days: 2, - url({ date }) { - return `${API_ENDPOINT}?fecha=${date.format('YYYY/MM/DD')}` - }, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - async parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const $item = cheerio.load(item) - const programId = parseProgramId($item) - const details = await loadProgramDetails(programId) - if (!details) continue - programs.push({ - title: details.main_title, - description: details.short_argument, - icon: parseIcon(details), - actors: parseActors(details), - rating: parseRating(details), - date: details.year, - start: parseStart(details), - stop: parseStop(details) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}?fecha=${dayjs().format('YYYY/MM/DD')}`) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(data) - - return $('img') - .map(function () { - return { - lang: 'es', - site_id: $(this).attr('alt').replace('&', '&'), - name: $(this).attr('alt') - } - }) - .get() - } -} - -function parseProgramId($item) { - return $item('*').data('schedule') -} - -function loadProgramDetails(programId) { - return axios - .get(`https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/${programId}`) - .then(r => r.data) - .catch(console.log) -} - -function parseRating(details) { - return details.parental_rating - ? { - system: 'MPAA', - value: details.parental_rating - } - : null -} - -function parseActors(details) { - return details.actors.split(', ') -} - -function parseIcon(details) { - return details.image ? `https://img-ns.s3.amazonaws.com/grid_data/${details.image}` : null -} - -function parseStart(details) { - return dayjs.tz(details.time_start, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseStop(details) { - return dayjs.tz(details.time_end, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - - return $(`img[alt="${channel.site_id}"]`).first().nextUntil('img').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.nuevosiglo.com.uy/programacion/getGrilla' + +module.exports = { + site: 'nuevosiglo.com.uy', + days: 2, + url({ date }) { + return `${API_ENDPOINT}?fecha=${date.format('YYYY/MM/DD')}` + }, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + async parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const $item = cheerio.load(item) + const programId = parseProgramId($item) + const details = await loadProgramDetails(programId) + if (!details) continue + programs.push({ + title: details.main_title, + description: details.short_argument, + icon: parseIcon(details), + actors: parseActors(details), + rating: parseRating(details), + date: details.year, + start: parseStart(details), + stop: parseStop(details) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}?fecha=${dayjs().format('YYYY/MM/DD')}`) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(data) + + return $('img') + .map(function () { + return { + lang: 'es', + site_id: $(this).attr('alt').replace('&', '&'), + name: $(this).attr('alt') + } + }) + .get() + } +} + +function parseProgramId($item) { + return $item('*').data('schedule') +} + +function loadProgramDetails(programId) { + return axios + .get(`https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/${programId}`) + .then(r => r.data) + .catch(console.log) +} + +function parseRating(details) { + return details.parental_rating + ? { + system: 'MPAA', + value: details.parental_rating + } + : null +} + +function parseActors(details) { + return details.actors.split(', ') +} + +function parseIcon(details) { + return details.image ? `https://img-ns.s3.amazonaws.com/grid_data/${details.image}` : null +} + +function parseStart(details) { + return dayjs.tz(details.time_start, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseStop(details) { + return dayjs.tz(details.time_end, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + + return $(`img[alt="${channel.site_id}"]`).first().nextUntil('img').toArray() +} diff --git a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js index 580326bb8..ba49859a6 100644 --- a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js +++ b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js @@ -1,100 +1,100 @@ -// npm run channels:parse -- --config=./sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js --output=./sites/nuevosiglo.com.uy/nuevosiglo.com.uy.channels.xml -// npm run grab -- --site=nuevosiglo.com.uy - -const { parser, url } = require('./nuevosiglo.com.uy.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-02-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'HBO', - xmltv_id: 'HBOLatinAmerica.us' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.nuevosiglo.com.uy/programacion/getGrilla?fecha=2023/02/10' - ) -}) - -it('can parse response', async () => { - axios.get.mockImplementation(url => { - if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769227') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769239') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = await parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-10T01:11:00.000Z', - stop: '2023-02-10T03:46:00.000Z', - title: 'Jurassic World: Dominion', - description: - 'Años después de la destrucción de Isla Nublar, los dinosaurios viven y cazan junto a los humanos. Este equilibrio determinará, si los humanos seguirán siendo los depredadores máximos en un planeta que comparten con las criaturas temibles.', - icon: 'https://img-ns.s3.amazonaws.com/grid_data/23354476.jpg', - date: '2022', - rating: { - system: 'MPAA', - value: 'PG-13' - }, - actors: ['Jeff Goldblum', 'Sam Neill', 'Bryce Dallas Howard'] - }) - - expect(results[1]).toMatchObject({ - start: '2023-02-11T02:06:00.000Z', - stop: '2023-02-11T04:16:00.000Z', - title: 'Black Adam', - description: - 'Black Adam es liberado de su tumba casi cinco mil años después de haber sido encarcelado y recibir sus poderes de los antiguos dioses. Ahora está listo para desatar su forma única de justicia en el mundo.', - icon: 'https://img-ns.s3.amazonaws.com/grid_data/24638423.jpg', - date: '2022', - rating: { - system: 'MPAA', - value: 'PG-13' - }, - actors: [ - 'Aldis Hodge', - 'Dwayne Johnson', - 'Noah Centineo', - 'Sarah Shahi', - 'Marwan Kenzari', - 'Pierce Brosnan', - 'Quintessa Swindell', - 'Mohammed Amer', - 'Bodhi Sabongui', - 'James Cusati-Moyer' - ] - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - channel, - content: '' - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js --output=./sites/nuevosiglo.com.uy/nuevosiglo.com.uy.channels.xml +// npm run grab -- --site=nuevosiglo.com.uy + +const { parser, url } = require('./nuevosiglo.com.uy.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-02-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'HBO', + xmltv_id: 'HBOLatinAmerica.us' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.nuevosiglo.com.uy/programacion/getGrilla?fecha=2023/02/10' + ) +}) + +it('can parse response', async () => { + axios.get.mockImplementation(url => { + if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769227') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769239') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = await parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-10T01:11:00.000Z', + stop: '2023-02-10T03:46:00.000Z', + title: 'Jurassic World: Dominion', + description: + 'Años después de la destrucción de Isla Nublar, los dinosaurios viven y cazan junto a los humanos. Este equilibrio determinará, si los humanos seguirán siendo los depredadores máximos en un planeta que comparten con las criaturas temibles.', + icon: 'https://img-ns.s3.amazonaws.com/grid_data/23354476.jpg', + date: '2022', + rating: { + system: 'MPAA', + value: 'PG-13' + }, + actors: ['Jeff Goldblum', 'Sam Neill', 'Bryce Dallas Howard'] + }) + + expect(results[1]).toMatchObject({ + start: '2023-02-11T02:06:00.000Z', + stop: '2023-02-11T04:16:00.000Z', + title: 'Black Adam', + description: + 'Black Adam es liberado de su tumba casi cinco mil años después de haber sido encarcelado y recibir sus poderes de los antiguos dioses. Ahora está listo para desatar su forma única de justicia en el mundo.', + icon: 'https://img-ns.s3.amazonaws.com/grid_data/24638423.jpg', + date: '2022', + rating: { + system: 'MPAA', + value: 'PG-13' + }, + actors: [ + 'Aldis Hodge', + 'Dwayne Johnson', + 'Noah Centineo', + 'Sarah Shahi', + 'Marwan Kenzari', + 'Pierce Brosnan', + 'Quintessa Swindell', + 'Mohammed Amer', + 'Bodhi Sabongui', + 'James Cusati-Moyer' + ] + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + channel, + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/ontvtonight.com/ontvtonight.com.config.js b/sites/ontvtonight.com/ontvtonight.com.config.js index 673be0647..fbe4b74e6 100644 --- a/sites/ontvtonight.com/ontvtonight.com.config.js +++ b/sites/ontvtonight.com/ontvtonight.com.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const tz = { - au: 'Australia/Sydney', - ie: 'Europe/Dublin', - uk: 'Europe/London', - us: 'America/New_York' -} - -module.exports = { - site: 'ontvtonight.com', - days: 2, - url: function ({ date, channel }) { - const [region, id] = channel.site_id.split('#') - let url = 'https://www.ontvtonight.com' - if (region) url += `/${region}` - url += `/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}` - - return url - }, - parser: function ({ content, date, channel }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date, channel) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date, channel) { - const [region] = channel.site_id.split('#') - const timezone = region ? tz[region] : tz['uk'] - const timeString = $item('td:nth-child(1) > h5').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', timezone) -} - -function parseTitle($item) { - return $item('td:nth-child(2) > h5').text().trim() -} - -function parseDescription($item) { - return $item('td:nth-child(2) > h6').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#content > div > div > div.span6 > table > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const tz = { + au: 'Australia/Sydney', + ie: 'Europe/Dublin', + uk: 'Europe/London', + us: 'America/New_York' +} + +module.exports = { + site: 'ontvtonight.com', + days: 2, + url: function ({ date, channel }) { + const [region, id] = channel.site_id.split('#') + let url = 'https://www.ontvtonight.com' + if (region) url += `/${region}` + url += `/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}` + + return url + }, + parser: function ({ content, date, channel }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date, channel) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date, channel) { + const [region] = channel.site_id.split('#') + const timezone = region ? tz[region] : tz['uk'] + const timeString = $item('td:nth-child(1) > h5').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', timezone) +} + +function parseTitle($item) { + return $item('td:nth-child(2) > h5').text().trim() +} + +function parseDescription($item) { + return $item('td:nth-child(2) > h6').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#content > div > div > div.span6 > table > tbody > tr').toArray() +} diff --git a/sites/ontvtonight.com/ontvtonight.com.test.js b/sites/ontvtonight.com/ontvtonight.com.test.js index 70b85b91e..ba9ef33ef 100644 --- a/sites/ontvtonight.com/ontvtonight.com.test.js +++ b/sites/ontvtonight.com/ontvtonight.com.test.js @@ -1,59 +1,59 @@ -// npm run grab -- --site=ontvtonight.com -// npm run grab -- --site=ontvtonight.com - -const { parser, url } = require('./ontvtonight.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'au#1692/7two', - xmltv_id: '7two.au' -} -const content = - '
    7TWO
    12:10 am
    What A Carry On
    12:50 am
    Bones
    The Devil In The Details
    10:50 pm
    Inspector Morse: The Remorseful Day
    ' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.ontvtonight.com/au/guide/listings/channel/1692/7two.html?dt=2021-11-25' - ) -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T13:10:00.000Z', - stop: '2021-11-24T13:50:00.000Z', - title: 'What A Carry On' - }, - { - start: '2021-11-24T13:50:00.000Z', - stop: '2021-11-25T11:50:00.000Z', - title: 'Bones', - description: 'The Devil In The Details' - }, - { - start: '2021-11-25T11:50:00.000Z', - stop: '2021-11-25T12:50:00.000Z', - title: 'Inspector Morse: The Remorseful Day' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=ontvtonight.com +// npm run grab -- --site=ontvtonight.com + +const { parser, url } = require('./ontvtonight.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'au#1692/7two', + xmltv_id: '7two.au' +} +const content = + '
    7TWO
    12:10 am
    What A Carry On
    12:50 am
    Bones
    The Devil In The Details
    10:50 pm
    Inspector Morse: The Remorseful Day
    ' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.ontvtonight.com/au/guide/listings/channel/1692/7two.html?dt=2021-11-25' + ) +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T13:10:00.000Z', + stop: '2021-11-24T13:50:00.000Z', + title: 'What A Carry On' + }, + { + start: '2021-11-24T13:50:00.000Z', + stop: '2021-11-25T11:50:00.000Z', + title: 'Bones', + description: 'The Devil In The Details' + }, + { + start: '2021-11-25T11:50:00.000Z', + stop: '2021-11-25T12:50:00.000Z', + title: 'Inspector Morse: The Remorseful Day' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/osn.com/osn.com.config.js b/sites/osn.com/osn.com.config.js index 955b07566..e1520c8b0 100644 --- a/sites/osn.com/osn.com.config.js +++ b/sites/osn.com/osn.com.config.js @@ -1,71 +1,71 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'osn.com', - days: 2, - url: 'https://www.osn.com/CMSPages/TVScheduleWebService.asmx/GetTVChannelsProgramTimeTable', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Referer: 'https://www.osn.com' - }, - data({ channel, date }) { - return { - newDate: date.format('MM/DD/YYYY'), - selectedCountry: 'AE', - channelCode: channel.site_id, - isMobile: false, - hoursForMobile: 0 - } - }, - jar: null - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item, channel) - const duration = parseDuration(item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle(item, channel), - category: parseCategory(item, channel), - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - } -} - -function parseTitle(item, channel) { - return channel.lang === 'ar' ? item.Arab_Title : item.Title -} - -function parseCategory(item, channel) { - return channel.lang === 'ar' ? item.GenreArabicName : item.GenreEnglishName -} - -function parseDuration(item) { - return parseInt(item.TotalDivWidth / 4.8) -} - -function parseStart(item) { - const time = item.StartDateTime - - return dayjs.tz(time, 'DD MMM YYYY, HH:mm', 'Asia/Dubai') -} - -function parseItems(content) { - if (!content) return [] - const json = JSON.parse(content) - - return json.d ? JSON.parse(json.d) : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'osn.com', + days: 2, + url: 'https://www.osn.com/CMSPages/TVScheduleWebService.asmx/GetTVChannelsProgramTimeTable', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Referer: 'https://www.osn.com' + }, + data({ channel, date }) { + return { + newDate: date.format('MM/DD/YYYY'), + selectedCountry: 'AE', + channelCode: channel.site_id, + isMobile: false, + hoursForMobile: 0 + } + }, + jar: null + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item, channel) + const duration = parseDuration(item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle(item, channel), + category: parseCategory(item, channel), + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + } +} + +function parseTitle(item, channel) { + return channel.lang === 'ar' ? item.Arab_Title : item.Title +} + +function parseCategory(item, channel) { + return channel.lang === 'ar' ? item.GenreArabicName : item.GenreEnglishName +} + +function parseDuration(item) { + return parseInt(item.TotalDivWidth / 4.8) +} + +function parseStart(item) { + const time = item.StartDateTime + + return dayjs.tz(time, 'DD MMM YYYY, HH:mm', 'Asia/Dubai') +} + +function parseItems(content) { + if (!content) return [] + const json = JSON.parse(content) + + return json.d ? JSON.parse(json.d) : [] +} diff --git a/sites/osn.com/osn.com.test.js b/sites/osn.com/osn.com.test.js index d1517d97d..48266025d 100644 --- a/sites/osn.com/osn.com.test.js +++ b/sites/osn.com/osn.com.test.js @@ -1,69 +1,69 @@ -// NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/osn.com/osn.com.config.js --channels=sites/osn.com/osn.com.channels.xml --output=guide.xml --days=2 - -const { parser, url, request } = require('./osn.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channelAR = { site_id: 'AAN', xmltv_id: 'AlAanTV.ae', lang: 'ar' } -const channelEN = { site_id: 'AAN', xmltv_id: 'AlAanTV.ae', lang: 'en' } -const content = JSON.stringify({ - d: '[{"IsPlaying":"0","Durationtime":null,"StartMinute":0,"EndMinute":0,"EmptyDivWidth":1152,"TotalDivWidth":576,"IsTodayDate":false,"IsLastRow":false,"StartDateTime":"24 Oct 2021, 22:00","EndDateTime":"\\/Date(-62135596800000)\\/","Title":"Al Aan TV","Arab_Title":"تلفزيون الآن","GenreEnglishName":null,"GenreArabicName":null,"ChannelNumber":140,"ChannelCode":"AAN","Duration":"\\/Date(-62135596800000)\\/","Showtime":"\\/Date(-62135596800000)\\/","EpisodeId":738257,"ProgramType":null,"EPGUNIQID":"AAN202110271800738257"}]' -}) - -it('can generate valid request data', () => { - const result = request.data({ channel: channelAR, date }) - expect(result).toMatchObject({ - newDate: '10/24/2021', - selectedCountry: 'AE', - channelCode: 'AAN', - isMobile: false, - hoursForMobile: 0 - }) -}) - -it('can generate valid request headers', () => { - const result = request.headers - expect(result).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8', - Referer: 'https://www.osn.com' - }) -}) - -it('can generate valid url', () => { - expect(url).toBe( - 'https://www.osn.com/CMSPages/TVScheduleWebService.asmx/GetTVChannelsProgramTimeTable' - ) -}) - -it('can parse response (ar)', () => { - const result = parser({ date, channel: channelAR, content }) - expect(result).toMatchObject([ - { - start: 'Sun, 24 Oct 2021 18:00:00 GMT', - stop: 'Sun, 24 Oct 2021 20:00:00 GMT', - title: 'تلفزيون الآن', - category: null - } - ]) -}) - -it('can parse response (en)', () => { - const result = parser({ date, channel: channelEN, content }) - expect(result).toMatchObject([ - { - start: 'Sun, 24 Oct 2021 18:00:00 GMT', - stop: 'Sun, 24 Oct 2021 20:00:00 GMT', - title: 'Al Aan TV', - category: null - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel: channelAR, content: JSON.stringify({ d: '[]' }) }) - expect(result).toMatchObject([]) -}) +// NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/osn.com/osn.com.config.js --channels=sites/osn.com/osn.com.channels.xml --output=guide.xml --days=2 + +const { parser, url, request } = require('./osn.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channelAR = { site_id: 'AAN', xmltv_id: 'AlAanTV.ae', lang: 'ar' } +const channelEN = { site_id: 'AAN', xmltv_id: 'AlAanTV.ae', lang: 'en' } +const content = JSON.stringify({ + d: '[{"IsPlaying":"0","Durationtime":null,"StartMinute":0,"EndMinute":0,"EmptyDivWidth":1152,"TotalDivWidth":576,"IsTodayDate":false,"IsLastRow":false,"StartDateTime":"24 Oct 2021, 22:00","EndDateTime":"\\/Date(-62135596800000)\\/","Title":"Al Aan TV","Arab_Title":"تلفزيون الآن","GenreEnglishName":null,"GenreArabicName":null,"ChannelNumber":140,"ChannelCode":"AAN","Duration":"\\/Date(-62135596800000)\\/","Showtime":"\\/Date(-62135596800000)\\/","EpisodeId":738257,"ProgramType":null,"EPGUNIQID":"AAN202110271800738257"}]' +}) + +it('can generate valid request data', () => { + const result = request.data({ channel: channelAR, date }) + expect(result).toMatchObject({ + newDate: '10/24/2021', + selectedCountry: 'AE', + channelCode: 'AAN', + isMobile: false, + hoursForMobile: 0 + }) +}) + +it('can generate valid request headers', () => { + const result = request.headers + expect(result).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8', + Referer: 'https://www.osn.com' + }) +}) + +it('can generate valid url', () => { + expect(url).toBe( + 'https://www.osn.com/CMSPages/TVScheduleWebService.asmx/GetTVChannelsProgramTimeTable' + ) +}) + +it('can parse response (ar)', () => { + const result = parser({ date, channel: channelAR, content }) + expect(result).toMatchObject([ + { + start: 'Sun, 24 Oct 2021 18:00:00 GMT', + stop: 'Sun, 24 Oct 2021 20:00:00 GMT', + title: 'تلفزيون الآن', + category: null + } + ]) +}) + +it('can parse response (en)', () => { + const result = parser({ date, channel: channelEN, content }) + expect(result).toMatchObject([ + { + start: 'Sun, 24 Oct 2021 18:00:00 GMT', + stop: 'Sun, 24 Oct 2021 20:00:00 GMT', + title: 'Al Aan TV', + category: null + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel: channelAR, content: JSON.stringify({ d: '[]' }) }) + expect(result).toMatchObject([]) +}) diff --git a/sites/pbsguam.org/pbsguam.org.config.js b/sites/pbsguam.org/pbsguam.org.config.js index 5f0ce2581..6784326c7 100644 --- a/sites/pbsguam.org/pbsguam.org.config.js +++ b/sites/pbsguam.org/pbsguam.org.config.js @@ -1,41 +1,41 @@ -const dayjs = require('dayjs') -const isBetween = require('dayjs/plugin/isBetween') - -dayjs.extend(isBetween) - -module.exports = { - site: 'pbsguam.org', - days: 2, // the program is only available Thursday through Sunday - url: 'https://pbsguam.org/calendar/', - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - programs.push({ - title: item.title, - start: dayjs(item.start), - stop: dayjs(item.end) - }) - }) - - return programs - } -} - -function parseItems(content, date) { - const [, json] = content.match(/EventsSchedule_1 = (.*);/i) || [null, ''] - let data - try { - data = JSON.parse(json) - } catch (error) { - return [] - } - - if (!data || !Array.isArray(data.feed)) return [] - - return data.feed.filter( - i => - dayjs(i.start).isBetween(date, date.add(1, 'd')) || - dayjs(i.end).isBetween(date, date.add(1, 'd')) - ) -} +const dayjs = require('dayjs') +const isBetween = require('dayjs/plugin/isBetween') + +dayjs.extend(isBetween) + +module.exports = { + site: 'pbsguam.org', + days: 2, // the program is only available Thursday through Sunday + url: 'https://pbsguam.org/calendar/', + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + programs.push({ + title: item.title, + start: dayjs(item.start), + stop: dayjs(item.end) + }) + }) + + return programs + } +} + +function parseItems(content, date) { + const [, json] = content.match(/EventsSchedule_1 = (.*);/i) || [null, ''] + let data + try { + data = JSON.parse(json) + } catch (error) { + return [] + } + + if (!data || !Array.isArray(data.feed)) return [] + + return data.feed.filter( + i => + dayjs(i.start).isBetween(date, date.add(1, 'd')) || + dayjs(i.end).isBetween(date, date.add(1, 'd')) + ) +} diff --git a/sites/pbsguam.org/pbsguam.org.test.js b/sites/pbsguam.org/pbsguam.org.test.js index 732b4f065..5d5b0a9be 100644 --- a/sites/pbsguam.org/pbsguam.org.test.js +++ b/sites/pbsguam.org/pbsguam.org.test.js @@ -1,48 +1,48 @@ -// npm run grab -- --site=pbsguam.org - -const { parser, url } = require('./pbsguam.org.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KGTF.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://pbsguam.org/calendar/') -}) - -it('can parse response', () => { - const content = ` ` - const result = parser({ date, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-25T08:30:00.000Z', - stop: '2021-11-25T09:00:00.000Z', - title: 'Xavier Riddle and the Secret Museum' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=pbsguam.org + +const { parser, url } = require('./pbsguam.org.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KGTF.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://pbsguam.org/calendar/') +}) + +it('can parse response', () => { + const content = ` ` + const result = parser({ date, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-25T08:30:00.000Z', + stop: '2021-11-25T09:00:00.000Z', + title: 'Xavier Riddle and the Secret Museum' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ' ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/plex.tv/plex.tv.config.js b/sites/plex.tv/plex.tv.config.js index ac4bd781f..72c784259 100644 --- a/sites/plex.tv/plex.tv.config.js +++ b/sites/plex.tv/plex.tv.config.js @@ -1,90 +1,90 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://epg.provider.plex.tv' - -module.exports = { - site: 'plex.tv', - days: 2, - request: { - headers: { - 'x-plex-provider-version': '5.1' - } - }, - url: function ({ channel, date }) { - const [, channelGridKey] = channel.site_id.split('-') - - return `${API_ENDPOINT}/grid?channelGridKey=${channelGridKey}&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content }) { - const programs = [] - const items = parseItems(content) - for (let item of items) { - programs.push({ - title: item.title, - description: item.summary, - categories: parseCategories(item), - icon: item.art, - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}/lineups/plex/channels?X-Plex-Token=zb85PfdNQLmsry9kQLBR`) - .then(r => r.data) - .catch(console.error) - - return data.MediaContainer.Channel.map(c => { - return { - site_id: c.id, - name: c.title - } - }) - } -} - -function parseCategories(item) { - return Array.isArray(item.Genre) ? item.Genre.map(g => g.tag) : [] -} - -function parseStart(item) { - return item.beginsAt ? dayjs.unix(item.beginsAt) : null -} - -function parseStop(item) { - return item.endsAt ? dayjs.unix(item.endsAt) : null -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.MediaContainer || !Array.isArray(data.MediaContainer.Metadata)) return [] - const metadata = data.MediaContainer.Metadata - const items = [] - metadata.forEach(item => { - let segments = [] - item.Media.sort(byTime).forEach(media => { - let prevSegment = segments[segments.length - 1] - if (prevSegment && prevSegment.endsAt === media.beginsAt) { - prevSegment.endsAt = media.endsAt - } else { - segments.push(media) - } - }) - - segments.forEach(segment => { - items.push({ ...item, segments, beginsAt: segment.beginsAt, endsAt: segment.endsAt }) - }) - }) - - return items.sort(byTime) - - function byTime(a, b) { - if (a.beginsAt > b.beginsAt) return 1 - if (a.beginsAt < b.beginsAt) return -1 - return 0 - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://epg.provider.plex.tv' + +module.exports = { + site: 'plex.tv', + days: 2, + request: { + headers: { + 'x-plex-provider-version': '5.1' + } + }, + url: function ({ channel, date }) { + const [, channelGridKey] = channel.site_id.split('-') + + return `${API_ENDPOINT}/grid?channelGridKey=${channelGridKey}&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content }) { + const programs = [] + const items = parseItems(content) + for (let item of items) { + programs.push({ + title: item.title, + description: item.summary, + categories: parseCategories(item), + icon: item.art, + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}/lineups/plex/channels?X-Plex-Token=zb85PfdNQLmsry9kQLBR`) + .then(r => r.data) + .catch(console.error) + + return data.MediaContainer.Channel.map(c => { + return { + site_id: c.id, + name: c.title + } + }) + } +} + +function parseCategories(item) { + return Array.isArray(item.Genre) ? item.Genre.map(g => g.tag) : [] +} + +function parseStart(item) { + return item.beginsAt ? dayjs.unix(item.beginsAt) : null +} + +function parseStop(item) { + return item.endsAt ? dayjs.unix(item.endsAt) : null +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.MediaContainer || !Array.isArray(data.MediaContainer.Metadata)) return [] + const metadata = data.MediaContainer.Metadata + const items = [] + metadata.forEach(item => { + let segments = [] + item.Media.sort(byTime).forEach(media => { + let prevSegment = segments[segments.length - 1] + if (prevSegment && prevSegment.endsAt === media.beginsAt) { + prevSegment.endsAt = media.endsAt + } else { + segments.push(media) + } + }) + + segments.forEach(segment => { + items.push({ ...item, segments, beginsAt: segment.beginsAt, endsAt: segment.endsAt }) + }) + }) + + return items.sort(byTime) + + function byTime(a, b) { + if (a.beginsAt > b.beginsAt) return 1 + if (a.beginsAt < b.beginsAt) return -1 + return 0 + } +} diff --git a/sites/plex.tv/plex.tv.test.js b/sites/plex.tv/plex.tv.test.js index 6f137e09f..ada8e73fa 100644 --- a/sites/plex.tv/plex.tv.test.js +++ b/sites/plex.tv/plex.tv.test.js @@ -1,59 +1,59 @@ -// npm run channels:parse -- --config=./sites/plex.tv/plex.tv.config.js --output=./sites/plex.tv/plex.tv.channels.xml -// npm run grab -- --site=plex.tv - -const { parser, url, request } = require('./plex.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-02-05', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', - xmltv_id: 'DarkMatterTV.us' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epg.provider.plex.tv/grid?channelGridKey=5eea605674085f0040ddc7a6&date=2023-02-05' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-plex-provider-version': '5.1' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - // expect(results.length).toBe(15) - expect(results[0]).toMatchObject({ - start: '2023-02-04T23:31:14.000Z', - stop: '2023-02-05T01:10:45.000Z', - title: 'Violet & Daisy', - description: - 'Two teenage assassins accept what they think will be a quick-and-easy job, until an unexpected target throws them off their plan.', - icon: 'https://provider-static.plex.tv/epg/images/ott_channels/arts/darkmatter-tv-about.jpg', - categories: ['Movies'] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ content }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/plex.tv/plex.tv.config.js --output=./sites/plex.tv/plex.tv.channels.xml +// npm run grab -- --site=plex.tv + +const { parser, url, request } = require('./plex.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-02-05', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', + xmltv_id: 'DarkMatterTV.us' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epg.provider.plex.tv/grid?channelGridKey=5eea605674085f0040ddc7a6&date=2023-02-05' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-plex-provider-version': '5.1' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + // expect(results.length).toBe(15) + expect(results[0]).toMatchObject({ + start: '2023-02-04T23:31:14.000Z', + stop: '2023-02-05T01:10:45.000Z', + title: 'Violet & Daisy', + description: + 'Two teenage assassins accept what they think will be a quick-and-easy job, until an unexpected target throws them off their plan.', + icon: 'https://provider-static.plex.tv/epg/images/ott_channels/arts/darkmatter-tv-about.jpg', + categories: ['Movies'] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js index be1be74a8..cf20319ed 100644 --- a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js +++ b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js @@ -1,105 +1,105 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'programacion-tv.elpais.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `https://programacion-tv.elpais.com/data/parrilla_${date.format('DDMMYYYY')}.json` - }, - parser: async function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - if (!items.length) return programs - const programsData = await loadProgramsData(channel) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - const details = programsData.find(p => p.id_programa === item.id_programa) || {} - programs.push({ - title: item.title, - sub_title: details.episode_title, - description: details.episode_description || item.description, - category: parseCategory(details), - icon: parseIcon(details), - director: parseList(details.director), - actors: parseList(details.actors), - writer: parseList(details.script), - producer: parseList(details.producer), - presenter: parseList(details.presented_by), - composer: parseList(details.music), - guest: parseList(details.guest_actors), - season: parseNumber(details.season), - episode: parseNumber(details.episode), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://programacion-tv.elpais.com/data/canales.json') - .then(r => r.data) - .catch(console.log) - - return Object.values(data).map(item => ({ - lang: 'es', - site_id: item.id, - name: item.nombre - })) - } -} - -function parseNumber(str) { - return typeof str === 'string' ? parseInt(str) : null -} - -function parseList(str) { - return typeof str === 'string' ? str.split(', ') : [] -} - -function parseIcon(details) { - const url = new URL(details.image, 'https://programacion-tv.elpais.com/') - - return url.href -} - -function parseCategory(details) { - return [details.txt_genre, details.txt_subgenre].filter(Boolean).join('/') -} - -async function loadProgramsData(channel) { - return await axios - .get(`https://programacion-tv.elpais.com/data/programas/${channel.site_id}.json`) - .then(r => r.data) - .catch(console.log) -} - -function parseStart(item) { - return dayjs.tz(item.iniDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') -} - -function parseStop(item) { - return dayjs.tz(item.endDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - const channelData = data.find(i => i.idCanal === channel.site_id) - if (!channelData || !Array.isArray(channelData.programas)) return [] - - return channelData.programas -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'programacion-tv.elpais.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `https://programacion-tv.elpais.com/data/parrilla_${date.format('DDMMYYYY')}.json` + }, + parser: async function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + if (!items.length) return programs + const programsData = await loadProgramsData(channel) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + const details = programsData.find(p => p.id_programa === item.id_programa) || {} + programs.push({ + title: item.title, + sub_title: details.episode_title, + description: details.episode_description || item.description, + category: parseCategory(details), + icon: parseIcon(details), + director: parseList(details.director), + actors: parseList(details.actors), + writer: parseList(details.script), + producer: parseList(details.producer), + presenter: parseList(details.presented_by), + composer: parseList(details.music), + guest: parseList(details.guest_actors), + season: parseNumber(details.season), + episode: parseNumber(details.episode), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://programacion-tv.elpais.com/data/canales.json') + .then(r => r.data) + .catch(console.log) + + return Object.values(data).map(item => ({ + lang: 'es', + site_id: item.id, + name: item.nombre + })) + } +} + +function parseNumber(str) { + return typeof str === 'string' ? parseInt(str) : null +} + +function parseList(str) { + return typeof str === 'string' ? str.split(', ') : [] +} + +function parseIcon(details) { + const url = new URL(details.image, 'https://programacion-tv.elpais.com/') + + return url.href +} + +function parseCategory(details) { + return [details.txt_genre, details.txt_subgenre].filter(Boolean).join('/') +} + +async function loadProgramsData(channel) { + return await axios + .get(`https://programacion-tv.elpais.com/data/programas/${channel.site_id}.json`) + .then(r => r.data) + .catch(console.log) +} + +function parseStart(item) { + return dayjs.tz(item.iniDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') +} + +function parseStop(item) { + return dayjs.tz(item.endDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + const channelData = data.find(i => i.idCanal === channel.site_id) + if (!channelData || !Array.isArray(channelData.programas)) return [] + + return channelData.programas +} diff --git a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js index c600e3276..e20019626 100644 --- a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js +++ b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js @@ -1,74 +1,74 @@ -// npm run grab -- --site=programacion-tv.elpais.com -// npm run channels:parse -- --config=./sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js --output=./sites/programacion-tv.elpais.com/programacion-tv.elpais.com.channels.xml - -const { parser, url } = require('./programacion-tv.elpais.com.config.js') -const axios = require('axios') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3', - xmltv_id: 'La1.es' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://programacion-tv.elpais.com/data/parrilla_04102022.json') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - axios.get.mockImplementation(url => { - if (url === 'https://programacion-tv.elpais.com/data/programas/3.json') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/programs.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results).toMatchObject([ - { - start: '2022-10-03T23:30:00.000Z', - stop: '2022-10-04T00:25:00.000Z', - title: 'Comerse el mundo', - sub_title: 'París', - description: - 'El chef Peña viaja hasta París, una de las capitales mundiales de la alta gastronomía. Allí visitará un viñedo muy especial en pleno corazón de la ciudad, probará los famosos caracoles, hará un queso y conocerá a chefs que llegaron a la capital gala para cumplir sus sueños y los consiguieron.', - director: ['Sergio Martín', 'Victor Arribas'], - presenter: ['Javier Peña'], - writer: ['Filippo Gravino', 'Guido Iuculano', 'Michele Pellegrini'], - actors: ['Pietro Sermonti', 'Maya Sansa', 'Ana Caterina Morariu'], - guest: ['Tobia de Angelis', 'Benedetta Porcaroli', 'Roberto Nocchi'], - producer: ['Javier Redondo'], - composer: ['Paco Musulén'], - category: 'Ocio-Cultura/Cocina', - season: 1, - episode: 23, - icon: 'https://programacion-tv.elpais.com/imagenes/programas/2099957.jpg' - } - ]) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=programacion-tv.elpais.com +// npm run channels:parse -- --config=./sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js --output=./sites/programacion-tv.elpais.com/programacion-tv.elpais.com.channels.xml + +const { parser, url } = require('./programacion-tv.elpais.com.config.js') +const axios = require('axios') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3', + xmltv_id: 'La1.es' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://programacion-tv.elpais.com/data/parrilla_04102022.json') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + axios.get.mockImplementation(url => { + if (url === 'https://programacion-tv.elpais.com/data/programas/3.json') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/programs.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results).toMatchObject([ + { + start: '2022-10-03T23:30:00.000Z', + stop: '2022-10-04T00:25:00.000Z', + title: 'Comerse el mundo', + sub_title: 'París', + description: + 'El chef Peña viaja hasta París, una de las capitales mundiales de la alta gastronomía. Allí visitará un viñedo muy especial en pleno corazón de la ciudad, probará los famosos caracoles, hará un queso y conocerá a chefs que llegaron a la capital gala para cumplir sus sueños y los consiguieron.', + director: ['Sergio Martín', 'Victor Arribas'], + presenter: ['Javier Peña'], + writer: ['Filippo Gravino', 'Guido Iuculano', 'Michele Pellegrini'], + actors: ['Pietro Sermonti', 'Maya Sansa', 'Ana Caterina Morariu'], + guest: ['Tobia de Angelis', 'Benedetta Porcaroli', 'Roberto Nocchi'], + producer: ['Javier Redondo'], + composer: ['Paco Musulén'], + category: 'Ocio-Cultura/Cocina', + season: 1, + episode: 23, + icon: 'https://programacion-tv.elpais.com/imagenes/programas/2099957.jpg' + } + ]) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js index 34716713a..b72115f14 100644 --- a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js +++ b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js @@ -1,102 +1,102 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/' - -module.exports = { - site: 'programacion.tcc.com.uy', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - maxContentLength: 10 * 1024 * 1024 // 30Mb - }, - url: function ({ date }) { - return `${API_ENDPOINT}?cable_operator=1&emission_start=${date.format( - 'YYYY-MM-DDTHH:mm:ss[Z]' - )}&emission_end=${date.add(1, 'd').format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` - }, - parser({ content, channel }) { - let programs = [] - let items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - categories: parseCategories(item), - date: item.year, - season: item.season_number, - episode: item.episode_number, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get( - `${API_ENDPOINT}?cable_operator=1&emission_start=${dayjs().format( - 'YYYY-MM-DDTHH:mm:ss[Z]' - )}&emission_end=${dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` - ) - .then(r => r.data) - .catch(console.error) - - return data.results.map(c => { - return { - lang: 'es', - site_id: c.id, - name: c.name.replace(/^\[.*\]\s/, '') - } - }) - } -} - -function parseTitle(item) { - const localized = item.localized.find(i => i.language === 'es') - - return localized ? localized.title : item.original_title -} - -function parseDescription(item) { - const localized = item.localized.find(i => i.language === 'es') - - return localized ? localized.description : null -} - -function parseCategories(item) { - return item.genres - .map(g => { - const localized = g.localized.find(i => i.language === 'es') - - return localized ? localized.name : null - }) - .filter(Boolean) -} - -function parseIcon(item) { - const uri = item.images[0] ? item.images[0].image_media.file : null - - return uri ? `https:${uri}` : null -} - -function parseStart(item) { - return dayjs(item.emission_start) -} - -function parseStop(item) { - return dayjs(item.emission_end) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.results)) return [] - const channelData = data.results.find(c => c.id == channel.site_id) - if (!channelData || !Array.isArray(channelData.events)) return [] - - return channelData.events -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/' + +module.exports = { + site: 'programacion.tcc.com.uy', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + maxContentLength: 10 * 1024 * 1024 // 30Mb + }, + url: function ({ date }) { + return `${API_ENDPOINT}?cable_operator=1&emission_start=${date.format( + 'YYYY-MM-DDTHH:mm:ss[Z]' + )}&emission_end=${date.add(1, 'd').format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` + }, + parser({ content, channel }) { + let programs = [] + let items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + categories: parseCategories(item), + date: item.year, + season: item.season_number, + episode: item.episode_number, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get( + `${API_ENDPOINT}?cable_operator=1&emission_start=${dayjs().format( + 'YYYY-MM-DDTHH:mm:ss[Z]' + )}&emission_end=${dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` + ) + .then(r => r.data) + .catch(console.error) + + return data.results.map(c => { + return { + lang: 'es', + site_id: c.id, + name: c.name.replace(/^\[.*\]\s/, '') + } + }) + } +} + +function parseTitle(item) { + const localized = item.localized.find(i => i.language === 'es') + + return localized ? localized.title : item.original_title +} + +function parseDescription(item) { + const localized = item.localized.find(i => i.language === 'es') + + return localized ? localized.description : null +} + +function parseCategories(item) { + return item.genres + .map(g => { + const localized = g.localized.find(i => i.language === 'es') + + return localized ? localized.name : null + }) + .filter(Boolean) +} + +function parseIcon(item) { + const uri = item.images[0] ? item.images[0].image_media.file : null + + return uri ? `https:${uri}` : null +} + +function parseStart(item) { + return dayjs(item.emission_start) +} + +function parseStop(item) { + return dayjs(item.emission_end) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.results)) return [] + const channelData = data.results.find(c => c.id == channel.site_id) + if (!channelData || !Array.isArray(channelData.events)) return [] + + return channelData.events +} diff --git a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js index cd79c8220..1fa7ced04 100644 --- a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js +++ b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js @@ -1,79 +1,79 @@ -// npm run channels:parse -- --config=./sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js --output=./sites/programacion.tcc.com.uy/programacion.tcc.com.uy.channels.xml -// npm run grab -- --site=programacion.tcc.com.uy - -const { parser, url } = require('./programacion.tcc.com.uy.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '212', - xmltv_id: 'MultiPremier.mx' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/?cable_operator=1&emission_start=2023-02-11T00:00:00Z&emission_end=2023-02-12T00:00:00Z&format=json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-10T22:45:00.000Z', - stop: '2023-02-11T00:30:00.000Z', - title: 'Meurtres à... - Temp. 3 - Episodio 3', - date: 2016, - season: 3, - episode: 3, - categories: [], - icon: 'https://zpapi.zetatv.com.uy/media/images/2b45d2675389f2e4f7f6fe0655ccc968.jpg', - description: - 'Cada episodio relata un lugar y una historia diferente pero siguiendo la línea de una investigación basada en una leyenda la cual es guiada por una pareja. Estos dos personajes no son necesariamente ambos policías, pero se ven obligados a colaborar a pesar de los primeros informes difíciles.' - }) - expect(results[1]).toMatchObject({ - start: '2023-02-11T00:30:00.000Z', - stop: '2023-02-11T03:00:00.000Z', - title: 'Grandes esperanzas', - date: 1998, - season: null, - episode: null, - categories: ['Drama'], - icon: 'https://zpapi.zetatv.com.uy/media/images/8cab42d88691edaa8a4001b91f809d91.jpg', - description: - 'Basada en la novela de Charles Dickens, cuenta la historia del pintor Finn que persigue obsesionado a su amor de la niñez, la bella y rica Estella. Gracias a un misterioso benefactor, Finn es enviado a Nueva York, donde se reúne con la hermosa y fría joven.' - }) - expect(results[3]).toMatchObject({ - start: '2023-02-11T05:35:00.000Z', - stop: '2023-02-11T07:45:00.000Z', - title: 'Los niños están bien', - date: 2010, - season: null, - episode: null, - categories: ['Comedia', 'Drama'], - icon: 'https://zpapi.zetatv.com.uy/media/images/51684d91ed33cb9b0c1863b7a9b097e9.jpg', - description: - 'Una pareja de lesbianas conciben a un niño y una niña por inseminacion artificial. Al paso del tiempo, los chicos deciden conocer a su verdadero padre a espaldas de sus madres. Tras localizarlo intentan integrar toda una familia. Podran lograrlo?.' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), - channel - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js --output=./sites/programacion.tcc.com.uy/programacion.tcc.com.uy.channels.xml +// npm run grab -- --site=programacion.tcc.com.uy + +const { parser, url } = require('./programacion.tcc.com.uy.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '212', + xmltv_id: 'MultiPremier.mx' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/?cable_operator=1&emission_start=2023-02-11T00:00:00Z&emission_end=2023-02-12T00:00:00Z&format=json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-10T22:45:00.000Z', + stop: '2023-02-11T00:30:00.000Z', + title: 'Meurtres à... - Temp. 3 - Episodio 3', + date: 2016, + season: 3, + episode: 3, + categories: [], + icon: 'https://zpapi.zetatv.com.uy/media/images/2b45d2675389f2e4f7f6fe0655ccc968.jpg', + description: + 'Cada episodio relata un lugar y una historia diferente pero siguiendo la línea de una investigación basada en una leyenda la cual es guiada por una pareja. Estos dos personajes no son necesariamente ambos policías, pero se ven obligados a colaborar a pesar de los primeros informes difíciles.' + }) + expect(results[1]).toMatchObject({ + start: '2023-02-11T00:30:00.000Z', + stop: '2023-02-11T03:00:00.000Z', + title: 'Grandes esperanzas', + date: 1998, + season: null, + episode: null, + categories: ['Drama'], + icon: 'https://zpapi.zetatv.com.uy/media/images/8cab42d88691edaa8a4001b91f809d91.jpg', + description: + 'Basada en la novela de Charles Dickens, cuenta la historia del pintor Finn que persigue obsesionado a su amor de la niñez, la bella y rica Estella. Gracias a un misterioso benefactor, Finn es enviado a Nueva York, donde se reúne con la hermosa y fría joven.' + }) + expect(results[3]).toMatchObject({ + start: '2023-02-11T05:35:00.000Z', + stop: '2023-02-11T07:45:00.000Z', + title: 'Los niños están bien', + date: 2010, + season: null, + episode: null, + categories: ['Comedia', 'Drama'], + icon: 'https://zpapi.zetatv.com.uy/media/images/51684d91ed33cb9b0c1863b7a9b097e9.jpg', + description: + 'Una pareja de lesbianas conciben a un niño y una niña por inseminacion artificial. Al paso del tiempo, los chicos deciden conocer a su verdadero padre a espaldas de sus madres. Tras localizarlo intentan integrar toda una familia. Podran lograrlo?.' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), + channel + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/programetv.ro/programetv.ro.config.js b/sites/programetv.ro/programetv.ro.config.js index 64f73227b..c1b3c2ff5 100644 --- a/sites/programetv.ro/programetv.ro.config.js +++ b/sites/programetv.ro/programetv.ro.config.js @@ -1,80 +1,80 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'programetv.ro', - days: 2, - url: function ({ date, channel }) { - const daysOfWeek = { - 0: 'duminica', - 1: 'luni', - 2: 'marti', - 3: 'miercuri', - 4: 'joi', - 5: 'vineri', - 6: 'sambata' - } - const day = date.day() - - return `https://www.programetv.ro/post/${channel.site_id}/${daysOfWeek[day]}/` - }, - parser: function ({ content }) { - let programs = [] - const data = parseContent(content) - if (!data || !data.shows) return programs - const items = data.shows - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.titleOriginal, - description: item.desc || item.obs, - category: item.categories, - season: item.season || null, - episode: item.episode || null, - start: parseStart(item), - stop: parseStop(item), - url: item.url || null, - date: item.date, - rating: parseRating(item), - directors: parseDirector(item), - actors: parseActor(item), - icon: item.icon - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs(item.start).toJSON() -} - -function parseStop(item) { - return dayjs(item.stop).toJSON() -} - -function parseContent(content) { - const [, data] = content.match(/var pageData = ((.|[\r\n])+);\n/) || [null, null] - - return data ? JSON.parse(data) : {} -} - -function parseDirector(item) { - return item.credits && item.credits.director ? item.credits.director : null -} - -function parseActor(item) { - return item.credits && item.credits.actor ? item.credits.actor : null -} - -function parseRating(item) { - return item.rating - ? { - system: 'CNC', - value: item.rating.toUpperCase() - } - : null -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'programetv.ro', + days: 2, + url: function ({ date, channel }) { + const daysOfWeek = { + 0: 'duminica', + 1: 'luni', + 2: 'marti', + 3: 'miercuri', + 4: 'joi', + 5: 'vineri', + 6: 'sambata' + } + const day = date.day() + + return `https://www.programetv.ro/post/${channel.site_id}/${daysOfWeek[day]}/` + }, + parser: function ({ content }) { + let programs = [] + const data = parseContent(content) + if (!data || !data.shows) return programs + const items = data.shows + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.titleOriginal, + description: item.desc || item.obs, + category: item.categories, + season: item.season || null, + episode: item.episode || null, + start: parseStart(item), + stop: parseStop(item), + url: item.url || null, + date: item.date, + rating: parseRating(item), + directors: parseDirector(item), + actors: parseActor(item), + icon: item.icon + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs(item.start).toJSON() +} + +function parseStop(item) { + return dayjs(item.stop).toJSON() +} + +function parseContent(content) { + const [, data] = content.match(/var pageData = ((.|[\r\n])+);\n/) || [null, null] + + return data ? JSON.parse(data) : {} +} + +function parseDirector(item) { + return item.credits && item.credits.director ? item.credits.director : null +} + +function parseActor(item) { + return item.credits && item.credits.actor ? item.credits.actor : null +} + +function parseRating(item) { + return item.rating + ? { + system: 'CNC', + value: item.rating.toUpperCase() + } + : null +} diff --git a/sites/programetv.ro/programetv.ro.test.js b/sites/programetv.ro/programetv.ro.test.js index c4e9690ea..e0245ec10 100644 --- a/sites/programetv.ro/programetv.ro.test.js +++ b/sites/programetv.ro/programetv.ro.test.js @@ -1,70 +1,70 @@ -// npm run grab -- --site=programetv.ro - -const { parser, url } = require('./programetv.ro.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'pro-tv', xmltv_id: 'ProTV.ro' } -const content = ` - - - - - -` - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://www.programetv.ro/post/pro-tv/duminica/') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-07T05:00:00.000Z', - stop: '2021-11-07T07:59:59.000Z', - title: 'Ştirile Pro Tv', - description: - 'În fiecare zi, cele mai importante evenimente, transmisiuni LIVE, analize, anchete şi reportaje sunt la Ştirile ProTV.', - category: ['Ştiri'], - icon: 'https://www.programetv.ro/img/shows/84/54/stirile-pro-tv.png?key=Z2lfZnVial90cmFyZXZwLzAwLzAwLzA1LzE4MzgxMnktMTIwazE3MC1hLW40NTk4MW9zLmNhdA==' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ` - - - - - - -` - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=programetv.ro + +const { parser, url } = require('./programetv.ro.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'pro-tv', xmltv_id: 'ProTV.ro' } +const content = ` + + + + + +` + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://www.programetv.ro/post/pro-tv/duminica/') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-07T05:00:00.000Z', + stop: '2021-11-07T07:59:59.000Z', + title: 'Ştirile Pro Tv', + description: + 'În fiecare zi, cele mai importante evenimente, transmisiuni LIVE, analize, anchete şi reportaje sunt la Ştirile ProTV.', + category: ['Ştiri'], + icon: 'https://www.programetv.ro/img/shows/84/54/stirile-pro-tv.png?key=Z2lfZnVial90cmFyZXZwLzAwLzAwLzA1LzE4MzgxMnktMTIwazE3MC1hLW40NTk4MW9zLmNhdA==' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ` + + + + + + +` + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/programme-tv.net/programme-tv.net.config.js b/sites/programme-tv.net/programme-tv.net.config.js index a566e9c6e..5231aa3bb 100644 --- a/sites/programme-tv.net/programme-tv.net.config.js +++ b/sites/programme-tv.net/programme-tv.net.config.js @@ -1,73 +1,73 @@ -const durationParser = require('parse-duration') -const cheerio = require('cheerio') -const srcset = require('srcset') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'programme-tv.net', - days: 2, - url: function ({ date, channel }) { - return `https://www.programme-tv.net/programme/chaine/${date.format('YYYY-MM-DD')}/programme-${ - channel.site_id - }.html` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - const icon = parseIcon($item) - const category = parseCategory($item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'ms') - - programs.push({ title, icon, category, start, stop }) - }) - - return programs - } -} - -function parseStart($item, date) { - let time = $item('.mainBroadcastCard-startingHour').first().text().trim() - time = `${date.format('MM/DD/YYYY')} ${time.replace('h', ':')}` - - return dayjs.tz(time, 'MM/DD/YYYY HH:mm', 'Europe/Paris') -} - -function parseDuration($item) { - const duration = $item('.mainBroadcastCard-durationContent').first().text().trim() - - return durationParser(duration) -} - -function parseIcon($item) { - const img = $item('.mainBroadcastCard-imageContent').first().find('img') - const value = img.attr('srcset') || img.data('srcset') - const obj = value ? srcset.parse(value).find(i => i.width === 128) : {} - - return obj.url -} - -function parseCategory($item) { - return $item('.mainBroadcastCard-genre').first().text().trim() -} - -function parseTitle($item) { - return $item('.mainBroadcastCard-title').first().text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.mainBroadcastCard').toArray() -} +const durationParser = require('parse-duration') +const cheerio = require('cheerio') +const srcset = require('srcset') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'programme-tv.net', + days: 2, + url: function ({ date, channel }) { + return `https://www.programme-tv.net/programme/chaine/${date.format('YYYY-MM-DD')}/programme-${ + channel.site_id + }.html` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + const icon = parseIcon($item) + const category = parseCategory($item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'ms') + + programs.push({ title, icon, category, start, stop }) + }) + + return programs + } +} + +function parseStart($item, date) { + let time = $item('.mainBroadcastCard-startingHour').first().text().trim() + time = `${date.format('MM/DD/YYYY')} ${time.replace('h', ':')}` + + return dayjs.tz(time, 'MM/DD/YYYY HH:mm', 'Europe/Paris') +} + +function parseDuration($item) { + const duration = $item('.mainBroadcastCard-durationContent').first().text().trim() + + return durationParser(duration) +} + +function parseIcon($item) { + const img = $item('.mainBroadcastCard-imageContent').first().find('img') + const value = img.attr('srcset') || img.data('srcset') + const obj = value ? srcset.parse(value).find(i => i.width === 128) : {} + + return obj.url +} + +function parseCategory($item) { + return $item('.mainBroadcastCard-genre').first().text().trim() +} + +function parseTitle($item) { + return $item('.mainBroadcastCard-title').first().text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.mainBroadcastCard').toArray() +} diff --git a/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js b/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js index c3a9c458f..11fcaf967 100644 --- a/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js +++ b/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js @@ -1,88 +1,88 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -const apiUrl = 'https://programme-tv.vini.pf/programmesJSON' - -module.exports = { - site: 'programme-tv.vini.pf', - days: 2, - url: apiUrl, - request: { - method: 'POST', - data({ date }) { - return { - dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` - } - } - }, - parser: async function ({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - if (items.length) { - for (let hours of [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]) { - const nextContent = await loadNextItems(date, hours) - const nextItems = parseItems(nextContent, channel) - for (let item of nextItems) { - if (!items.find(i => i.nidP === item.nidP)) { - items.push(item) - } - } - } - } - - items.forEach(item => { - programs.push({ - title: item.titreP, - description: item.desc, - category: item.categorieP, - icon: item.srcP, - start: dayjs.unix(item.timestampDeb), - stop: dayjs.unix(item.timestampFin) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .post('https://programme-tv.vini.pf/programmesJSON') - .then(r => r.data) - .catch(console.log) - - return data.programmes.map(item => { - const channelId = item.url.replace('/', '') - return { - lang: 'fr', - site_id: channelId, - name: channelId - } - }) - } -} - -async function loadNextItems(date, hours) { - date = date.add(hours, 'h') - - return axios - .post( - apiUrl, - { - dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` - }, - { - responseType: 'arraybuffer' - } - ) - .then(res => res.data.toString()) - .catch(console.log) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programmes)) return [] - const channelData = data.programmes.find(i => i.url === `/${channel.site_id}`) - if (!channelData) return [] - - return channelData.programmes || [] -} +const dayjs = require('dayjs') +const axios = require('axios') + +const apiUrl = 'https://programme-tv.vini.pf/programmesJSON' + +module.exports = { + site: 'programme-tv.vini.pf', + days: 2, + url: apiUrl, + request: { + method: 'POST', + data({ date }) { + return { + dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` + } + } + }, + parser: async function ({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + if (items.length) { + for (let hours of [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]) { + const nextContent = await loadNextItems(date, hours) + const nextItems = parseItems(nextContent, channel) + for (let item of nextItems) { + if (!items.find(i => i.nidP === item.nidP)) { + items.push(item) + } + } + } + } + + items.forEach(item => { + programs.push({ + title: item.titreP, + description: item.desc, + category: item.categorieP, + icon: item.srcP, + start: dayjs.unix(item.timestampDeb), + stop: dayjs.unix(item.timestampFin) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .post('https://programme-tv.vini.pf/programmesJSON') + .then(r => r.data) + .catch(console.log) + + return data.programmes.map(item => { + const channelId = item.url.replace('/', '') + return { + lang: 'fr', + site_id: channelId, + name: channelId + } + }) + } +} + +async function loadNextItems(date, hours) { + date = date.add(hours, 'h') + + return axios + .post( + apiUrl, + { + dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` + }, + { + responseType: 'arraybuffer' + } + ) + .then(res => res.data.toString()) + .catch(console.log) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programmes)) return [] + const channelData = data.programmes.find(i => i.url === `/${channel.site_id}`) + if (!channelData) return [] + + return channelData.programmes || [] +} diff --git a/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js b/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js index 453fbe6b2..46329b4b5 100644 --- a/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js +++ b/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js @@ -1,110 +1,110 @@ -// npm run channels:parse -- --config=./sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js --output=./sites/programme-tv.vini.pf/programme-tv.vini.pf.channels.xml -// npm run grab -- --site=programme-tv.vini.pf - -const { parser, url, request } = require('./programme-tv.vini.pf.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tf1', - xmltv_id: 'TF1.fr' -} - -it('can generate valid url', () => { - expect(url).toBe('https://programme-tv.vini.pf/programmesJSON') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request data', () => { - expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' }) -}) - -it('can parse response', done => { - axios.post.mockImplementation((url, data) => { - if (data.dateDebut === '2021-11-20T16:00:00-10:00') { - return Promise.resolve({ - data: Buffer.from( - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":25,"active":false,"progression":0,"test":0,"nowphp":1637509998},{"nidP":"24162438","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"50mn Inside","heureP":"16:30","timestampDeb":1637461800,"timestampFin":1637466300,"altP":"","titleP":"","legendeP":"L\'actu","desc":"50\'INSIDE, c\'est toute l\'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg","urlP":"/50mn-inside-20112021-1630","width":62.5,"active":false,"progression":0,"test":0,"nowphp":1637509998}]}]}' - ) - }) - } else { - return Promise.resolve({ - data: Buffer.from( - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' - ) - }) - } - }) - - const content = - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162436","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Reportages découverte","heureP":"13:50","timestampDeb":1637452200,"timestampFin":1637457000,"altP":"","titleP":"","legendeP":"La coloc ne connaît pas la crise","desc":"Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d\'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg","urlP":"/reportages-decouverte-20112021-1350","width":58.333333333333,"active":false,"progression":0,"test":0,"nowphp":1637509179},{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":41.666666666667,"active":false,"progression":0,"test":0,"nowphp":1637509179}]}]}' - - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-20T23:50:00.000Z', - stop: '2021-11-21T01:10:00.000Z', - title: 'Reportages découverte', - category: 'Magazine', - description: - "Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.", - icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg' - }, - { - start: '2021-11-21T01:10:00.000Z', - stop: '2021-11-21T02:30:00.000Z', - title: 'Les docs du week-end', - category: 'Magazine', - description: - 'Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?', - icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg' - }, - { - start: '2021-11-21T02:30:00.000Z', - stop: '2021-11-21T03:45:00.000Z', - title: '50mn Inside', - category: 'Magazine', - description: - "50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,", - icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg' - } - ]) - done() - }) - .catch(err => { - done(err) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(err => { - done(err) - }) -}) +// npm run channels:parse -- --config=./sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js --output=./sites/programme-tv.vini.pf/programme-tv.vini.pf.channels.xml +// npm run grab -- --site=programme-tv.vini.pf + +const { parser, url, request } = require('./programme-tv.vini.pf.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tf1', + xmltv_id: 'TF1.fr' +} + +it('can generate valid url', () => { + expect(url).toBe('https://programme-tv.vini.pf/programmesJSON') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request data', () => { + expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' }) +}) + +it('can parse response', done => { + axios.post.mockImplementation((url, data) => { + if (data.dateDebut === '2021-11-20T16:00:00-10:00') { + return Promise.resolve({ + data: Buffer.from( + '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":25,"active":false,"progression":0,"test":0,"nowphp":1637509998},{"nidP":"24162438","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"50mn Inside","heureP":"16:30","timestampDeb":1637461800,"timestampFin":1637466300,"altP":"","titleP":"","legendeP":"L\'actu","desc":"50\'INSIDE, c\'est toute l\'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg","urlP":"/50mn-inside-20112021-1630","width":62.5,"active":false,"progression":0,"test":0,"nowphp":1637509998}]}]}' + ) + }) + } else { + return Promise.resolve({ + data: Buffer.from( + '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' + ) + }) + } + }) + + const content = + '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162436","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Reportages découverte","heureP":"13:50","timestampDeb":1637452200,"timestampFin":1637457000,"altP":"","titleP":"","legendeP":"La coloc ne connaît pas la crise","desc":"Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d\'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg","urlP":"/reportages-decouverte-20112021-1350","width":58.333333333333,"active":false,"progression":0,"test":0,"nowphp":1637509179},{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":41.666666666667,"active":false,"progression":0,"test":0,"nowphp":1637509179}]}]}' + + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-20T23:50:00.000Z', + stop: '2021-11-21T01:10:00.000Z', + title: 'Reportages découverte', + category: 'Magazine', + description: + "Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.", + icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg' + }, + { + start: '2021-11-21T01:10:00.000Z', + stop: '2021-11-21T02:30:00.000Z', + title: 'Les docs du week-end', + category: 'Magazine', + description: + 'Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?', + icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg' + }, + { + start: '2021-11-21T02:30:00.000Z', + stop: '2021-11-21T03:45:00.000Z', + title: '50mn Inside', + category: 'Magazine', + description: + "50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,", + icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg' + } + ]) + done() + }) + .catch(err => { + done(err) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: + '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(err => { + done(err) + }) +}) diff --git a/sites/programme.tvb.com/programme.tvb.com.config.js b/sites/programme.tvb.com/programme.tvb.com.config.js index 7fa1d4fe9..16aea97a6 100644 --- a/sites/programme.tvb.com/programme.tvb.com.config.js +++ b/sites/programme.tvb.com/programme.tvb.com.config.js @@ -1,64 +1,64 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'programme.tvb.com', - days: 2, - url: function ({ channel, date }) { - return `https://programme.tvb.com/ajax.php?action=channellist&code=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.ftit').text().trim() -} - -function parseDescription($item) { - return $item('.full').text().trim() -} - -function parseStart($item, date) { - const time = $item('.time').text() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD hh:mmA', 'Asia/Hong_Kong') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('ul > li.item').toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'programme.tvb.com', + days: 2, + url: function ({ channel, date }) { + return `https://programme.tvb.com/ajax.php?action=channellist&code=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.ftit').text().trim() +} + +function parseDescription($item) { + return $item('.full').text().trim() +} + +function parseStart($item, date) { + const time = $item('.time').text() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD hh:mmA', 'Asia/Hong_Kong') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('ul > li.item').toArray() +} diff --git a/sites/programme.tvb.com/programme.tvb.com.test.js b/sites/programme.tvb.com/programme.tvb.com.test.js index a17a7b29f..18b1e39fd 100644 --- a/sites/programme.tvb.com/programme.tvb.com.test.js +++ b/sites/programme.tvb.com/programme.tvb.com.test.js @@ -1,47 +1,47 @@ -// npm run grab -- --site=programme.tvb.com - -const { parser, url } = require('./programme.tvb.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'B', - xmltv_id: 'J2.hk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://programme.tvb.com/ajax.php?action=channellist&code=B&date=2022-11-15' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: '想見你#3[粵/普][PG]', - description: - '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=programme.tvb.com + +const { parser, url } = require('./programme.tvb.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'B', + xmltv_id: 'J2.hk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://programme.tvb.com/ajax.php?action=channellist&code=B&date=2022-11-15' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: '想見你#3[粵/普][PG]', + description: + '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/programtv.onet.pl/programtv.onet.pl.config.js b/sites/programtv.onet.pl/programtv.onet.pl.config.js index 9028de3c6..bffe3ee37 100644 --- a/sites/programtv.onet.pl/programtv.onet.pl.config.js +++ b/sites/programtv.onet.pl/programtv.onet.pl.config.js @@ -1,65 +1,65 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - delay: 5000, - site: 'programtv.onet.pl', - days: 2, - url: function ({ date, channel }) { - const currDate = DateTime.now().toUTC().startOf('day') - const day = date.diff(currDate, 'd') - - return `https://programtv.onet.pl/program-tv/${channel.site_id}?dzien=${day}` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('.hours > .hour').text() - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Warsaw' }).toUTC() -} - -function parseCategory($item) { - return $item('.titles > .type').text() -} - -function parseDescription($item) { - return $item('.titles > p').text().trim() -} - -function parseTitle($item) { - return $item('.titles > a').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#channelTV > section > div.emissions > ul > li').toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + delay: 5000, + site: 'programtv.onet.pl', + days: 2, + url: function ({ date, channel }) { + const currDate = DateTime.now().toUTC().startOf('day') + const day = date.diff(currDate, 'd') + + return `https://programtv.onet.pl/program-tv/${channel.site_id}?dzien=${day}` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('.hours > .hour').text() + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Warsaw' }).toUTC() +} + +function parseCategory($item) { + return $item('.titles > .type').text() +} + +function parseDescription($item) { + return $item('.titles > p').text().trim() +} + +function parseTitle($item) { + return $item('.titles > a').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#channelTV > section > div.emissions > ul > li').toArray() +} diff --git a/sites/programtv.onet.pl/programtv.onet.pl.test.js b/sites/programtv.onet.pl/programtv.onet.pl.test.js index 1f991e63b..dbfbc6202 100644 --- a/sites/programtv.onet.pl/programtv.onet.pl.test.js +++ b/sites/programtv.onet.pl/programtv.onet.pl.test.js @@ -1,77 +1,77 @@ -// npm run grab -- --site=programtv.onet.pl - -const MockDate = require('mockdate') -const { parser, url } = require('./programtv.onet.pl.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '13th-street-250', - xmltv_id: '13thStreet.de' -} -const content = - '
    13th Street
    • 03:20
      Law & Order, odc. 15: Letzte Worte Krimiserie

      Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....

    • 23:30
      Navy CIS, odc. 1: New Orleans Krimiserie

      Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...

    • 01:00
      Navy CIS: L.A, odc. 13: High Society Krimiserie

      Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...

    ' - -it('can generate valid url', () => { - MockDate.set(dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d')) - expect(url({ channel, date })).toBe( - 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=0' - ) - MockDate.reset() -}) - -it('can generate valid url for next day', () => { - MockDate.set(dayjs.utc('2021-11-23', 'YYYY-MM-DD').startOf('d')) - expect(url({ channel, date })).toBe( - 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=1' - ) - MockDate.reset() -}) - -it('can parse response', () => { - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T02:20:00.000Z', - stop: '2021-11-24T22:30:00.000Z', - title: 'Law & Order, odc. 15: Letzte Worte', - category: 'Krimiserie', - description: - 'Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....' - }, - { - start: '2021-11-24T22:30:00.000Z', - stop: '2021-11-25T00:00:00.000Z', - title: 'Navy CIS, odc. 1: New Orleans', - category: 'Krimiserie', - description: - 'Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...' - }, - { - start: '2021-11-25T00:00:00.000Z', - stop: '2021-11-25T01:00:00.000Z', - title: 'Navy CIS: L.A, odc. 13: High Society', - category: 'Krimiserie', - description: - 'Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=programtv.onet.pl + +const MockDate = require('mockdate') +const { parser, url } = require('./programtv.onet.pl.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '13th-street-250', + xmltv_id: '13thStreet.de' +} +const content = + '
    13th Street
    • 03:20
      Law & Order, odc. 15: Letzte Worte Krimiserie

      Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....

    • 23:30
      Navy CIS, odc. 1: New Orleans Krimiserie

      Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...

    • 01:00
      Navy CIS: L.A, odc. 13: High Society Krimiserie

      Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...

    ' + +it('can generate valid url', () => { + MockDate.set(dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d')) + expect(url({ channel, date })).toBe( + 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=0' + ) + MockDate.reset() +}) + +it('can generate valid url for next day', () => { + MockDate.set(dayjs.utc('2021-11-23', 'YYYY-MM-DD').startOf('d')) + expect(url({ channel, date })).toBe( + 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=1' + ) + MockDate.reset() +}) + +it('can parse response', () => { + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T02:20:00.000Z', + stop: '2021-11-24T22:30:00.000Z', + title: 'Law & Order, odc. 15: Letzte Worte', + category: 'Krimiserie', + description: + 'Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....' + }, + { + start: '2021-11-24T22:30:00.000Z', + stop: '2021-11-25T00:00:00.000Z', + title: 'Navy CIS, odc. 1: New Orleans', + category: 'Krimiserie', + description: + 'Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...' + }, + { + start: '2021-11-25T00:00:00.000Z', + stop: '2021-11-25T01:00:00.000Z', + title: 'Navy CIS: L.A, odc. 13: High Society', + category: 'Krimiserie', + description: + 'Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/proximusmwc.be/proximusmwc.be.config.js b/sites/proximusmwc.be/proximusmwc.be.config.js index d61f01f7f..c07899ff6 100644 --- a/sites/proximusmwc.be/proximusmwc.be.config.js +++ b/sites/proximusmwc.be/proximusmwc.be.config.js @@ -1,80 +1,80 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'proximusmwc.be', - days: 2, - url: 'https://api.proximusmwc.be/v2/graphql', - request: { - headers: { - 'Content-Type': 'application/json' - }, - data({ channel, date }) { - return { - query: - 'query ($language: String!, $startTime: Int!, $endTime: Int!, $options: SchedulesByIntervalOptions) { schedulesByInterval(language: $language, startTime: $startTime, endTime: $endTime, options: $options) { trailId programReferenceNumber channelId title category startTime endTime image { key url __typename } parentalRating detailUrl grouped description shortDescription category categoryId subCategory links { episodeNumber id seasonId seasonName seriesId seriesTitle title type __typename } seriesId __typename }}', - variables: { - startTime: date.unix(), - endTime: date.add(1, 'd').unix(), - language: 'fr', - options: { channelIds: [channel.site_id] } - } - } - } - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - icon: parseIcon(item), - category: parseCategory(item), - start: dayjs.unix(item.startTime), - stop: dayjs.unix(item.endTime) - }) - }) - - return programs - }, - async channels() { - const query = { - operationName: 'getPlayableChannels', - variables: { language: 'fr', id: '0' }, - query: - 'query getPlayableChannels($language: String!, $queryParams: ChannelQueryParams, $id: String) { playableChannels(language: $language, queryParams: $queryParams, id: $id) { id name language radio }}' - } - const data = await axios - .post('https://api.proximusmwc.be/v2/graphql', query) - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let item of data.data.playableChannels) { - if (item.radio) continue - channels.push({ - lang: item.language, - site_id: item.id, - name: item.name - }) - } - - return channels - } -} - -function parseCategory(item) { - return item.category ? item.category.replace(/^C\./, '') : null -} - -function parseIcon(item) { - return item.image[0] ? item.image[0].url : null -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.data || !Array.isArray(data.data.schedulesByInterval)) return [] - - return data.data.schedulesByInterval -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'proximusmwc.be', + days: 2, + url: 'https://api.proximusmwc.be/v2/graphql', + request: { + headers: { + 'Content-Type': 'application/json' + }, + data({ channel, date }) { + return { + query: + 'query ($language: String!, $startTime: Int!, $endTime: Int!, $options: SchedulesByIntervalOptions) { schedulesByInterval(language: $language, startTime: $startTime, endTime: $endTime, options: $options) { trailId programReferenceNumber channelId title category startTime endTime image { key url __typename } parentalRating detailUrl grouped description shortDescription category categoryId subCategory links { episodeNumber id seasonId seasonName seriesId seriesTitle title type __typename } seriesId __typename }}', + variables: { + startTime: date.unix(), + endTime: date.add(1, 'd').unix(), + language: 'fr', + options: { channelIds: [channel.site_id] } + } + } + } + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + icon: parseIcon(item), + category: parseCategory(item), + start: dayjs.unix(item.startTime), + stop: dayjs.unix(item.endTime) + }) + }) + + return programs + }, + async channels() { + const query = { + operationName: 'getPlayableChannels', + variables: { language: 'fr', id: '0' }, + query: + 'query getPlayableChannels($language: String!, $queryParams: ChannelQueryParams, $id: String) { playableChannels(language: $language, queryParams: $queryParams, id: $id) { id name language radio }}' + } + const data = await axios + .post('https://api.proximusmwc.be/v2/graphql', query) + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let item of data.data.playableChannels) { + if (item.radio) continue + channels.push({ + lang: item.language, + site_id: item.id, + name: item.name + }) + } + + return channels + } +} + +function parseCategory(item) { + return item.category ? item.category.replace(/^C\./, '') : null +} + +function parseIcon(item) { + return item.image[0] ? item.image[0].url : null +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.data || !Array.isArray(data.data.schedulesByInterval)) return [] + + return data.data.schedulesByInterval +} diff --git a/sites/proximusmwc.be/proximusmwc.be.test.js b/sites/proximusmwc.be/proximusmwc.be.test.js index 491d8f0fa..8cbbb239a 100644 --- a/sites/proximusmwc.be/proximusmwc.be.test.js +++ b/sites/proximusmwc.be/proximusmwc.be.test.js @@ -1,69 +1,69 @@ -// npm run channels:parse -- --config=./sites/proximusmwc.be/proximusmwc.be.config.js --output=./sites/proximusmwc.be/proximusmwc.be.channels.xml -// npm run grab -- --site=proximusmwc.be - -const { parser, url, request } = require('./proximusmwc.be.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'UID0024', - xmltv_id: 'DasErste.de' -} - -it('can generate valid url', () => { - expect(url).toBe('https://api.proximusmwc.be/v2/graphql') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - query: - 'query ($language: String!, $startTime: Int!, $endTime: Int!, $options: SchedulesByIntervalOptions) { schedulesByInterval(language: $language, startTime: $startTime, endTime: $endTime, options: $options) { trailId programReferenceNumber channelId title category startTime endTime image { key url __typename } parentalRating detailUrl grouped description shortDescription category categoryId subCategory links { episodeNumber id seasonId seasonName seriesId seriesTitle title type __typename } seriesId __typename }}', - variables: { - endTime: 1646438400, - language: 'fr', - options: { channelIds: ['UID0024'] }, - startTime: 1646352000 - } - }) -}) - -it('can parse response', () => { - const content = - '{"data":{"schedulesByInterval":[{"trailId":"UID0024_202202225537","programReferenceNumber":"107504040014","channelId":"UID0024","title":"Der Bozen-Krimi","category":"C.Magazine","startTime":1646350800,"endTime":1646356200,"description":"Chiara Schoras alias \\"Capo\\" Sonja Schwarz muss im 14. Bozen-Krimi nicht nur einen widersprüchlichen Mordfall aufklären, sondern auch ein Geheimnis ans Licht bringen, das zwei Familien auf schmerzhafte Weise untrennbar verbindet.","image":[{"key":"source","url":"https://experience-cache.proximustv.be:443/posterserver/poster/EPG/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg","__typename":"Image"},{"key":"custom","url":"https://experience-cache.proximustv.be:443/posterserver/poster/EPG/w-%width%_h-%height%/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg","__typename":"Image"}]}]}}' - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-03T23:40:00.000Z', - stop: '2022-03-04T01:10:00.000Z', - title: 'Der Bozen-Krimi', - description: - 'Chiara Schoras alias "Capo" Sonja Schwarz muss im 14. Bozen-Krimi nicht nur einen widersprüchlichen Mordfall aufklären, sondern auch ein Geheimnis ans Licht bringen, das zwei Familien auf schmerzhafte Weise untrennbar verbindet.', - category: 'Magazine', - icon: 'https://experience-cache.proximustv.be:443/posterserver/poster/EPG/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"data":{"schedulesByInterval":[]}}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/proximusmwc.be/proximusmwc.be.config.js --output=./sites/proximusmwc.be/proximusmwc.be.channels.xml +// npm run grab -- --site=proximusmwc.be + +const { parser, url, request } = require('./proximusmwc.be.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'UID0024', + xmltv_id: 'DasErste.de' +} + +it('can generate valid url', () => { + expect(url).toBe('https://api.proximusmwc.be/v2/graphql') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + query: + 'query ($language: String!, $startTime: Int!, $endTime: Int!, $options: SchedulesByIntervalOptions) { schedulesByInterval(language: $language, startTime: $startTime, endTime: $endTime, options: $options) { trailId programReferenceNumber channelId title category startTime endTime image { key url __typename } parentalRating detailUrl grouped description shortDescription category categoryId subCategory links { episodeNumber id seasonId seasonName seriesId seriesTitle title type __typename } seriesId __typename }}', + variables: { + endTime: 1646438400, + language: 'fr', + options: { channelIds: ['UID0024'] }, + startTime: 1646352000 + } + }) +}) + +it('can parse response', () => { + const content = + '{"data":{"schedulesByInterval":[{"trailId":"UID0024_202202225537","programReferenceNumber":"107504040014","channelId":"UID0024","title":"Der Bozen-Krimi","category":"C.Magazine","startTime":1646350800,"endTime":1646356200,"description":"Chiara Schoras alias \\"Capo\\" Sonja Schwarz muss im 14. Bozen-Krimi nicht nur einen widersprüchlichen Mordfall aufklären, sondern auch ein Geheimnis ans Licht bringen, das zwei Familien auf schmerzhafte Weise untrennbar verbindet.","image":[{"key":"source","url":"https://experience-cache.proximustv.be:443/posterserver/poster/EPG/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg","__typename":"Image"},{"key":"custom","url":"https://experience-cache.proximustv.be:443/posterserver/poster/EPG/w-%width%_h-%height%/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg","__typename":"Image"}]}]}}' + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-03T23:40:00.000Z', + stop: '2022-03-04T01:10:00.000Z', + title: 'Der Bozen-Krimi', + description: + 'Chiara Schoras alias "Capo" Sonja Schwarz muss im 14. Bozen-Krimi nicht nur einen widersprüchlichen Mordfall aufklären, sondern auch ein Geheimnis ans Licht bringen, das zwei Familien auf schmerzhafte Weise untrennbar verbindet.', + category: 'Magazine', + icon: 'https://experience-cache.proximustv.be:443/posterserver/poster/EPG/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"data":{"schedulesByInterval":[]}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/raiplay.it/raiplay.it.config.js b/sites/raiplay.it/raiplay.it.config.js index 803c68b7f..27b562d67 100644 --- a/sites/raiplay.it/raiplay.it.config.js +++ b/sites/raiplay.it/raiplay.it.config.js @@ -1,79 +1,79 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -module.exports = { - site: 'raiplay.it', - days: 2, - url: function ({ date, channel }) { - return `https://www.raiplay.it/palinsesto/app/${channel.site_id}/${date.format( - 'DD-MM-YYYY' - )}.json` - }, - parser: function ({ content, date }) { - const programs = [] - const data = JSON.parse(content) - if (!data.events) return programs - - data.events.forEach(item => { - if (!item.name || !item.hour || !item.duration_in_minutes) return - const start = parseStart(item, date) - const duration = parseInt(item.duration_in_minutes) - const stop = start.add(duration, 'm') - - programs.push({ - title: item.name || item.program.name, - description: item.description, - season: parseSeason(item), - episode: parseEpisode(item), - sub_title: item['episode_title'] || null, - url: parseURL(item), - start, - stop, - icon: parseIcon(item) - }) - }) - - return programs - } -} - -function parseStart(item, date) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.hour}`, 'YYYY-MM-DD HH:mm', 'Europe/Rome') -} - -function parseIcon(item) { - let cover = null - if (item.image) { - cover = `https://www.raiplay.it${item.image}` - } - return cover -} - -function parseURL(item) { - let url = null - if (item.weblink) { - url = `https://www.raiplay.it${item.weblink}` - } - if (item.event_weblink) { - url = `https://www.raiplay.it${item.event_weblink}` - } - return url -} - -function parseSeason(item) { - if (!item.season) return null - if (String(item.season).length > 2) return null - return item.season -} - -function parseEpisode(item) { - if (!item.episode) return null - if (String(item.episode).length > 3) return null - return item.episode -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +module.exports = { + site: 'raiplay.it', + days: 2, + url: function ({ date, channel }) { + return `https://www.raiplay.it/palinsesto/app/${channel.site_id}/${date.format( + 'DD-MM-YYYY' + )}.json` + }, + parser: function ({ content, date }) { + const programs = [] + const data = JSON.parse(content) + if (!data.events) return programs + + data.events.forEach(item => { + if (!item.name || !item.hour || !item.duration_in_minutes) return + const start = parseStart(item, date) + const duration = parseInt(item.duration_in_minutes) + const stop = start.add(duration, 'm') + + programs.push({ + title: item.name || item.program.name, + description: item.description, + season: parseSeason(item), + episode: parseEpisode(item), + sub_title: item['episode_title'] || null, + url: parseURL(item), + start, + stop, + icon: parseIcon(item) + }) + }) + + return programs + } +} + +function parseStart(item, date) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.hour}`, 'YYYY-MM-DD HH:mm', 'Europe/Rome') +} + +function parseIcon(item) { + let cover = null + if (item.image) { + cover = `https://www.raiplay.it${item.image}` + } + return cover +} + +function parseURL(item) { + let url = null + if (item.weblink) { + url = `https://www.raiplay.it${item.weblink}` + } + if (item.event_weblink) { + url = `https://www.raiplay.it${item.event_weblink}` + } + return url +} + +function parseSeason(item) { + if (!item.season) return null + if (String(item.season).length > 2) return null + return item.season +} + +function parseEpisode(item) { + if (!item.episode) return null + if (String(item.episode).length > 3) return null + return item.episode +} diff --git a/sites/raiplay.it/raiplay.it.test.js b/sites/raiplay.it/raiplay.it.test.js index a7a2cc4f0..3e820074d 100644 --- a/sites/raiplay.it/raiplay.it.test.js +++ b/sites/raiplay.it/raiplay.it.test.js @@ -1,51 +1,51 @@ -// npm run grab -- --site=raiplay.it - -const { parser, url } = require('./raiplay.it.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-03', 'YYYY-MM-DD') -const channel = { - site_id: 'rai-2', - xmltv_id: 'Rai2.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json') -}) - -it('can parse response', () => { - const content = - '{ "id": "Page-e120a813-1b92-4057-a214-15943d95aa68", "title": "Pagina Palinsesto", "channel": "Rai 2", "date": "03-05-2022", "events": [ { "id": "ContentItem-2f81030d-803b-456a-9ea5-40233234fd9d", "name": "The Good Doctor S3E5 - La prima volta", "episode_title": "La prima volta", "episode": "5", "season": "3", "description": "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un\'idea geniale, sarà Andrews a portare a termine l\'operazione.", "channel": "Rai 2", "date": "03/05/2022", "hour": "19:40", "duration": "00:50:00", "duration_in_minutes": "50 min", "path_id": "", "weblink": "", "event_weblink": "/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html", "has_video": false, "image": "/dl/img/2020/03/09/1583748471860_dddddd.jpg", "playlist_id": "11430689", "program": { "name": "The Good Doctor", "path_id": "/programmi/thegooddoctor.json", "info_url": "/programmi/info/757edeac-6fff-4dea-afcd-0bcb39f9ea83.json", "weblink": "/programmi/thegooddoctor" } } ], "track_info": { "id": "", "domain": "raiplay", "platform": "[platform]", "media_type": "", "page_type": "", "editor": "raiplay", "year": "2019", "edit_year": "", "section": "guida tv", "sub_section": "rai 2", "content": "guida tv", "title": "", "channel": "", "date": "2019-09-08", "typology": "", "genres": [], "sub_genres": [], "program_title": "", "program_typology": "", "program_genres": [], "program_sub_genres": [], "edition": "", "season": "", "episode_number": "", "episode_title": "", "form": "", "listaDateMo": [], "dfp": {} }}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-05-03T17:40:00.000Z', - stop: '2022-05-03T18:30:00.000Z', - title: 'The Good Doctor S3E5 - La prima volta', - description: - "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.", - season: '3', - episode: '5', - sub_title: 'La prima volta', - icon: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg', - url: 'https://www.raiplay.it/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"events":[],"total":0}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=raiplay.it + +const { parser, url } = require('./raiplay.it.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-03', 'YYYY-MM-DD') +const channel = { + site_id: 'rai-2', + xmltv_id: 'Rai2.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json') +}) + +it('can parse response', () => { + const content = + '{ "id": "Page-e120a813-1b92-4057-a214-15943d95aa68", "title": "Pagina Palinsesto", "channel": "Rai 2", "date": "03-05-2022", "events": [ { "id": "ContentItem-2f81030d-803b-456a-9ea5-40233234fd9d", "name": "The Good Doctor S3E5 - La prima volta", "episode_title": "La prima volta", "episode": "5", "season": "3", "description": "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un\'idea geniale, sarà Andrews a portare a termine l\'operazione.", "channel": "Rai 2", "date": "03/05/2022", "hour": "19:40", "duration": "00:50:00", "duration_in_minutes": "50 min", "path_id": "", "weblink": "", "event_weblink": "/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html", "has_video": false, "image": "/dl/img/2020/03/09/1583748471860_dddddd.jpg", "playlist_id": "11430689", "program": { "name": "The Good Doctor", "path_id": "/programmi/thegooddoctor.json", "info_url": "/programmi/info/757edeac-6fff-4dea-afcd-0bcb39f9ea83.json", "weblink": "/programmi/thegooddoctor" } } ], "track_info": { "id": "", "domain": "raiplay", "platform": "[platform]", "media_type": "", "page_type": "", "editor": "raiplay", "year": "2019", "edit_year": "", "section": "guida tv", "sub_section": "rai 2", "content": "guida tv", "title": "", "channel": "", "date": "2019-09-08", "typology": "", "genres": [], "sub_genres": [], "program_title": "", "program_typology": "", "program_genres": [], "program_sub_genres": [], "edition": "", "season": "", "episode_number": "", "episode_title": "", "form": "", "listaDateMo": [], "dfp": {} }}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-05-03T17:40:00.000Z', + stop: '2022-05-03T18:30:00.000Z', + title: 'The Good Doctor S3E5 - La prima volta', + description: + "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.", + season: '3', + episode: '5', + sub_title: 'La prima volta', + icon: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg', + url: 'https://www.raiplay.it/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"events":[],"total":0}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/reportv.com.ar/reportv.com.ar.config.js b/sites/reportv.com.ar/reportv.com.ar.config.js index 974fe8916..afc68c681 100644 --- a/sites/reportv.com.ar/reportv.com.ar.config.js +++ b/sites/reportv.com.ar/reportv.com.ar.config.js @@ -1,169 +1,169 @@ -require('dayjs/locale/es') -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const _ = require('lodash') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'reportv.com.ar', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data({ channel, date }) { - const formData = new URLSearchParams() - formData.append('idSenial', channel.site_id) - formData.append('Alineacion', '2694') - formData.append('DiaDesde', date.format('YYYY/MM/DD')) - formData.append('HoraDesde', '00:00:00') - - return formData - } - }, - url: 'https://www.reportv.com.ar/buscador/ProgXSenial.php', - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - for (let item of items) { - const $item = cheerio.load(item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 's') - const details = await loadProgramDetails($item) - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - icon: details.icon, - description: details.description, - directors: details.directors, - actors: details.actors, - start, - stop - }) - } - - return programs - }, - async channels() { - const content = await axios - .get('https://www.reportv.com.ar/buscador/Buscador.php?aid=2694') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(content) - const items = $('#tr_home_2 > td:nth-child(1) > select > option').toArray() - - return items.map(item => { - return { - site_id: $(item).attr('value'), - name: $(item).text() - } - }) - } -} - -async function loadProgramDetails($item) { - const onclick = $item('*').attr('onclick') - const regexp = /detallePrograma\((\d+),(\d+),(\d+),(\d+),'([^']+)'\);/g - const match = [...onclick.matchAll(regexp)] - const [, id, idc, id_alineacion, idp, title] = match[0] - if (!id || !idc || !id_alineacion || !idp || !title) return Promise.resolve({}) - const formData = new URLSearchParams() - formData.append('id', id) - formData.append('idc', idc) - formData.append('id_alineacion', id_alineacion) - formData.append('idp', idp) - formData.append('title', title) - const content = await axios - .post('https://www.reportv.com.ar/buscador/DetallePrograma.php', formData) - .then(r => r.data.toString()) - .catch(console.error) - if (!content) return Promise.resolve({}) - - const $ = cheerio.load(content) - - return Promise.resolve({ - icon: parseIcon($), - actors: parseActors($), - directors: parseDirectors($), - description: parseDescription($) - }) -} - -function parseActors($) { - const section = $('#Ficha > div') - .html() - .split('
    ') - .find(str => str.includes('Actores:')) - if (!section) return null - const $section = cheerio.load(section) - - return $section('span') - .map((i, el) => $(el).text().trim()) - .get() -} - -function parseDirectors($) { - const section = $('#Ficha > div') - .html() - .split('
    ') - .find(str => str.includes('Directores:')) - if (!section) return null - const $section = cheerio.load(section) - - return $section('span') - .map((i, el) => $(el).text().trim()) - .get() -} - -function parseDescription($) { - return $('#Sinopsis > div').text().trim() -} - -function parseIcon($) { - const src = $('#ImgProg').attr('src') - const url = new URL(src, 'https://www.reportv.com.ar/buscador/') - - return url.href -} - -function parseTitle($item) { - const [, title] = $item('div:nth-child(1) > span').text().split(' - ') - - return title -} - -function parseCategory($item) { - return $item('div:nth-child(3) > span').text() -} - -function parseStart($item, date) { - const [time] = $item('div:nth-child(1) > span').text().split(' - ') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Caracas') -} - -function parseDuration($item) { - const [hh, mm, ss] = $item('div:nth-child(4) > span').text().split(':') - - return parseInt(hh) * 3600 + parseInt(mm) * 60 + parseInt(ss) -} - -function parseItems(content, date) { - if (!content) return [] - const $ = cheerio.load(content) - const d = _.startCase(date.locale('es').format('DD MMMM YYYY')) - - return $(`.trProg[title*="${d}"]`).toArray() -} +require('dayjs/locale/es') +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const _ = require('lodash') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'reportv.com.ar', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data({ channel, date }) { + const formData = new URLSearchParams() + formData.append('idSenial', channel.site_id) + formData.append('Alineacion', '2694') + formData.append('DiaDesde', date.format('YYYY/MM/DD')) + formData.append('HoraDesde', '00:00:00') + + return formData + } + }, + url: 'https://www.reportv.com.ar/buscador/ProgXSenial.php', + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + for (let item of items) { + const $item = cheerio.load(item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 's') + const details = await loadProgramDetails($item) + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + icon: details.icon, + description: details.description, + directors: details.directors, + actors: details.actors, + start, + stop + }) + } + + return programs + }, + async channels() { + const content = await axios + .get('https://www.reportv.com.ar/buscador/Buscador.php?aid=2694') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(content) + const items = $('#tr_home_2 > td:nth-child(1) > select > option').toArray() + + return items.map(item => { + return { + site_id: $(item).attr('value'), + name: $(item).text() + } + }) + } +} + +async function loadProgramDetails($item) { + const onclick = $item('*').attr('onclick') + const regexp = /detallePrograma\((\d+),(\d+),(\d+),(\d+),'([^']+)'\);/g + const match = [...onclick.matchAll(regexp)] + const [, id, idc, id_alineacion, idp, title] = match[0] + if (!id || !idc || !id_alineacion || !idp || !title) return Promise.resolve({}) + const formData = new URLSearchParams() + formData.append('id', id) + formData.append('idc', idc) + formData.append('id_alineacion', id_alineacion) + formData.append('idp', idp) + formData.append('title', title) + const content = await axios + .post('https://www.reportv.com.ar/buscador/DetallePrograma.php', formData) + .then(r => r.data.toString()) + .catch(console.error) + if (!content) return Promise.resolve({}) + + const $ = cheerio.load(content) + + return Promise.resolve({ + icon: parseIcon($), + actors: parseActors($), + directors: parseDirectors($), + description: parseDescription($) + }) +} + +function parseActors($) { + const section = $('#Ficha > div') + .html() + .split('
    ') + .find(str => str.includes('Actores:')) + if (!section) return null + const $section = cheerio.load(section) + + return $section('span') + .map((i, el) => $(el).text().trim()) + .get() +} + +function parseDirectors($) { + const section = $('#Ficha > div') + .html() + .split('
    ') + .find(str => str.includes('Directores:')) + if (!section) return null + const $section = cheerio.load(section) + + return $section('span') + .map((i, el) => $(el).text().trim()) + .get() +} + +function parseDescription($) { + return $('#Sinopsis > div').text().trim() +} + +function parseIcon($) { + const src = $('#ImgProg').attr('src') + const url = new URL(src, 'https://www.reportv.com.ar/buscador/') + + return url.href +} + +function parseTitle($item) { + const [, title] = $item('div:nth-child(1) > span').text().split(' - ') + + return title +} + +function parseCategory($item) { + return $item('div:nth-child(3) > span').text() +} + +function parseStart($item, date) { + const [time] = $item('div:nth-child(1) > span').text().split(' - ') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Caracas') +} + +function parseDuration($item) { + const [hh, mm, ss] = $item('div:nth-child(4) > span').text().split(':') + + return parseInt(hh) * 3600 + parseInt(mm) * 60 + parseInt(ss) +} + +function parseItems(content, date) { + if (!content) return [] + const $ = cheerio.load(content) + const d = _.startCase(date.locale('es').format('DD MMMM YYYY')) + + return $(`.trProg[title*="${d}"]`).toArray() +} diff --git a/sites/reportv.com.ar/reportv.com.ar.test.js b/sites/reportv.com.ar/reportv.com.ar.test.js index 44367883b..731b95000 100644 --- a/sites/reportv.com.ar/reportv.com.ar.test.js +++ b/sites/reportv.com.ar/reportv.com.ar.test.js @@ -1,114 +1,114 @@ -// npm run grab -- --site=reportv.com.ar -// npm run channels:parse -- --config=./sites/reportv.com.ar/reportv.com.ar.config.js --output=./sites/reportv.com.ar/reportv.com.ar.channels.xml - -const { parser, url, request } = require('./reportv.com.ar.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) -const axios = require('axios') -jest.mock('axios') - -const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '914', - xmltv_id: 'VePlusVenezuela.ve' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.reportv.com.ar/buscador/ProgXSenial.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.get('idSenial')).toBe('914') - expect(result.get('Alineacion')).toBe('2694') - expect(result.get('DiaDesde')).toBe('2022/10/03') - expect(result.get('HoraDesde')).toBe('00:00:00') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - axios.post.mockImplementation((url, data) => { - if ( - url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && - data.get('id') == '286096' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program1.html')) - }) - } else if ( - url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && - data.get('id') == '392803' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program2.html')) - }) - } else { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/no_program.html')) - }) - } - }) - - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-03T04:00:00.000Z', - stop: '2022-10-03T05:00:00.000Z', - title: '¿Quién tiene la razón?', - category: 'Talk Show', - icon: 'https://www.reportv.com.ar/buscador/img/Programas/4401882.jpg', - actors: ['Nancy Álvarez'], - description: - 'Espacio que dará de qué hablar cuando la doctora Nancy Álvarez y Carmen Jara, acompañadas de un jurado implacable, lleguen a escuchar y a resolver los problemas de las partes en conflicto para luego decidir quién tiene la razón.' - }) - - expect(results[21]).toMatchObject({ - start: '2022-10-04T03:00:00.000Z', - stop: '2022-10-04T04:00:00.000Z', - title: 'Valeria', - category: 'Comedia', - icon: 'https://www.reportv.com.ar/buscador/img/Programas/18788047.jpg', - directors: ['Inma Torrente'], - actors: [ - 'Diana Gómez', - 'Silma López', - 'Paula Malia', - 'Teresa Riott', - 'Maxi Iglesias', - 'Juanlu González', - 'Aitor Luna', - 'Lauren McFall', - 'Éva Martin', - 'Raquel Ventosa' - ], - description: - 'Valeria es una escritora que no está pasando por su mejor momento a nivel profesional y sentimental. La distancia emocional que la separa de su marido la lleva a refugiarse en sus tres mejores amigas: Carmen, Lola y Nerea. Valeria y sus amigas están inmersas en un torbellino de emociones de amor, amistad, celos, infidelidad, dudas, desamor, secretos, trabajo, preocupaciones, alegrías y sueños sobre el futuro.' - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = await parser({ content, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=reportv.com.ar +// npm run channels:parse -- --config=./sites/reportv.com.ar/reportv.com.ar.config.js --output=./sites/reportv.com.ar/reportv.com.ar.channels.xml + +const { parser, url, request } = require('./reportv.com.ar.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) +const axios = require('axios') +jest.mock('axios') + +const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '914', + xmltv_id: 'VePlusVenezuela.ve' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.reportv.com.ar/buscador/ProgXSenial.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.get('idSenial')).toBe('914') + expect(result.get('Alineacion')).toBe('2694') + expect(result.get('DiaDesde')).toBe('2022/10/03') + expect(result.get('HoraDesde')).toBe('00:00:00') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + axios.post.mockImplementation((url, data) => { + if ( + url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && + data.get('id') == '286096' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program1.html')) + }) + } else if ( + url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && + data.get('id') == '392803' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program2.html')) + }) + } else { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/no_program.html')) + }) + } + }) + + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-03T04:00:00.000Z', + stop: '2022-10-03T05:00:00.000Z', + title: '¿Quién tiene la razón?', + category: 'Talk Show', + icon: 'https://www.reportv.com.ar/buscador/img/Programas/4401882.jpg', + actors: ['Nancy Álvarez'], + description: + 'Espacio que dará de qué hablar cuando la doctora Nancy Álvarez y Carmen Jara, acompañadas de un jurado implacable, lleguen a escuchar y a resolver los problemas de las partes en conflicto para luego decidir quién tiene la razón.' + }) + + expect(results[21]).toMatchObject({ + start: '2022-10-04T03:00:00.000Z', + stop: '2022-10-04T04:00:00.000Z', + title: 'Valeria', + category: 'Comedia', + icon: 'https://www.reportv.com.ar/buscador/img/Programas/18788047.jpg', + directors: ['Inma Torrente'], + actors: [ + 'Diana Gómez', + 'Silma López', + 'Paula Malia', + 'Teresa Riott', + 'Maxi Iglesias', + 'Juanlu González', + 'Aitor Luna', + 'Lauren McFall', + 'Éva Martin', + 'Raquel Ventosa' + ], + description: + 'Valeria es una escritora que no está pasando por su mejor momento a nivel profesional y sentimental. La distancia emocional que la separa de su marido la lleva a refugiarse en sus tres mejores amigas: Carmen, Lola y Nerea. Valeria y sus amigas están inmersas en un torbellino de emociones de amor, amistad, celos, infidelidad, dudas, desamor, secretos, trabajo, preocupaciones, alegrías y sueños sobre el futuro.' + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = await parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/rev.bs/rev.bs.config.js b/sites/rev.bs/rev.bs.config.js index 454a02f4b..67dcd7bdd 100644 --- a/sites/rev.bs/rev.bs.config.js +++ b/sites/rev.bs/rev.bs.config.js @@ -1,68 +1,68 @@ -const _ = require('lodash') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'rev.bs', - days: 2, - url: function ({ date }) { - return `https://www.rev.bs/wp-content/uploads/tv-guide/${date.format('YYYY-MM-DD')}_0.json` - }, - parser: async function ({ content, channel, date }) { - const programs = [] - const items0 = parseItems(content, channel) - if (!items0.length) return programs - const items1 = parseItems(await loadNextItems(date, 1), channel) - const items2 = parseItems(await loadNextItems(date, 2), channel) - const items3 = parseItems(await loadNextItems(date, 3), channel) - const items = _.unionBy(items0, items1, items2, items3, 'sid') - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.duration, 'm') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -async function loadNextItems(date, index) { - const url = `https://www.rev.bs/wp-content/uploads/tv-guide/${date.format( - 'YYYY-MM-DD' - )}_${index}.json` - - return axios - .get(url, { - responseType: 'arraybuffer' - }) - .then(res => res.data.toString()) - .catch(console.log) -} - -function parseStart(item, date) { - const shift = parseInt(item.s) - - return dayjs.tz(date.add(shift, 'm').toString(), 'America/New_York') -} - -function parseItems(content, channel) { - let data - try { - data = JSON.parse(content) - } catch (error) { - return [] - } - - if (!data || data.status !== 'OK') return [] - - return data.data.schedule[channel.site_id] || [] -} +const _ = require('lodash') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'rev.bs', + days: 2, + url: function ({ date }) { + return `https://www.rev.bs/wp-content/uploads/tv-guide/${date.format('YYYY-MM-DD')}_0.json` + }, + parser: async function ({ content, channel, date }) { + const programs = [] + const items0 = parseItems(content, channel) + if (!items0.length) return programs + const items1 = parseItems(await loadNextItems(date, 1), channel) + const items2 = parseItems(await loadNextItems(date, 2), channel) + const items3 = parseItems(await loadNextItems(date, 3), channel) + const items = _.unionBy(items0, items1, items2, items3, 'sid') + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.duration, 'm') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +async function loadNextItems(date, index) { + const url = `https://www.rev.bs/wp-content/uploads/tv-guide/${date.format( + 'YYYY-MM-DD' + )}_${index}.json` + + return axios + .get(url, { + responseType: 'arraybuffer' + }) + .then(res => res.data.toString()) + .catch(console.log) +} + +function parseStart(item, date) { + const shift = parseInt(item.s) + + return dayjs.tz(date.add(shift, 'm').toString(), 'America/New_York') +} + +function parseItems(content, channel) { + let data + try { + data = JSON.parse(content) + } catch (error) { + return [] + } + + if (!data || data.status !== 'OK') return [] + + return data.data.schedule[channel.site_id] || [] +} diff --git a/sites/rev.bs/rev.bs.test.js b/sites/rev.bs/rev.bs.test.js index 96897deff..d03b261b7 100644 --- a/sites/rev.bs/rev.bs.test.js +++ b/sites/rev.bs/rev.bs.test.js @@ -1,83 +1,83 @@ -// npm run channels:parse -- --config=./sites/rev.bs/rev.bs.config.js --output=./sites/rev.bs/rev.bs.channels.xml -// npm run grab -- --site=rev.bs - -const { parser, url } = require('./rev.bs.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '206', - xmltv_id: 'WTVJ.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.rev.bs/wp-content/uploads/tv-guide/2021-11-21_0.json' - ) -}) - -it('can parse response', done => { - axios.get.mockImplementation(url => { - if (url === 'https://www.rev.bs/wp-content/uploads/tv-guide/2021-11-21_1.json') { - return Promise.resolve({ - data: Buffer.from( - '{"status":"OK","data":{"schedule":{"206":[{"title":"Talk Stoop","sid":43599836,"s":"330.0000","duration":30,"rating":"TVPG"}]}}}' - ) - }) - } else { - return Promise.resolve({ - data: Buffer.from('{"status":"OK","data":{"schedule":{}}}') - }) - } - }) - - const content = - '{"status":"OK","data":{"schedule":{"205":[{"title":"Rev Pulse 5 - Online Classifieds","sid":43576112,"s":"-120.0000","duration":120,"rating":""}],"206":[{"title":"Saturday Night Live","sid":43599827,"s":"-31.0000","duration":93,"rating":"TV14"}]}}}' - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-21T04:29:00.000Z', - stop: '2021-11-21T06:02:00.000Z', - title: 'Saturday Night Live' - }, - { - start: '2021-11-21T10:30:00.000Z', - stop: '2021-11-21T11:00:00.000Z', - title: 'Talk Stoop' - } - ]) - done() - }) - .catch(err => { - done(err) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: '' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(err => { - done(err) - }) -}) +// npm run channels:parse -- --config=./sites/rev.bs/rev.bs.config.js --output=./sites/rev.bs/rev.bs.channels.xml +// npm run grab -- --site=rev.bs + +const { parser, url } = require('./rev.bs.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '206', + xmltv_id: 'WTVJ.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.rev.bs/wp-content/uploads/tv-guide/2021-11-21_0.json' + ) +}) + +it('can parse response', done => { + axios.get.mockImplementation(url => { + if (url === 'https://www.rev.bs/wp-content/uploads/tv-guide/2021-11-21_1.json') { + return Promise.resolve({ + data: Buffer.from( + '{"status":"OK","data":{"schedule":{"206":[{"title":"Talk Stoop","sid":43599836,"s":"330.0000","duration":30,"rating":"TVPG"}]}}}' + ) + }) + } else { + return Promise.resolve({ + data: Buffer.from('{"status":"OK","data":{"schedule":{}}}') + }) + } + }) + + const content = + '{"status":"OK","data":{"schedule":{"205":[{"title":"Rev Pulse 5 - Online Classifieds","sid":43576112,"s":"-120.0000","duration":120,"rating":""}],"206":[{"title":"Saturday Night Live","sid":43599827,"s":"-31.0000","duration":93,"rating":"TV14"}]}}}' + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-21T04:29:00.000Z', + stop: '2021-11-21T06:02:00.000Z', + title: 'Saturday Night Live' + }, + { + start: '2021-11-21T10:30:00.000Z', + stop: '2021-11-21T11:00:00.000Z', + title: 'Talk Stoop' + } + ]) + done() + }) + .catch(err => { + done(err) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: '' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(err => { + done(err) + }) +}) diff --git a/sites/rotana.net/rotana.net.config.js b/sites/rotana.net/rotana.net.config.js index 32e5e9735..6ad86acb0 100644 --- a/sites/rotana.net/rotana.net.config.js +++ b/sites/rotana.net/rotana.net.config.js @@ -1,66 +1,66 @@ -const stream = require('stream') -const csv = require('csv-parser') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'rotana.net', - days: 2, - url({ channel }) { - return `https://rotana.net/triAssets/uploads/2020/11/${channel.site_id}.csv` - }, - request: { - method: 'POST' - }, - parser: async function ({ buffer, date }) { - let programs = [] - const items = await parseItems(buffer, date) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title: item['Arabic Event Name'], - category: item['Genre'], - description: item['Arabic Extended Description'], - start: start.toJSON(), - stop: stop.toJSON() - }) - }) - - return programs - } -} - -function parseStart(item) { - const time = `${item['Start Date']} ${item['Start Time']}` - - return dayjs.utc(time, 'DD/MM/YYYY HH:mm:ss:00') -} - -function parseStop(item) { - const time = `${item['End Date']} ${item['End Time']}` - - return dayjs.utc(time, 'DD/MM/YYYY HH:mm:ss:00') -} - -function parseItems(buffer, date) { - return new Promise(resolve => { - let items = [] - const input = new stream.PassThrough() - input.end(buffer) - input - .pipe(csv()) - .on('data', data => items.push(data)) - .on('end', () => { - items = items.filter(i => i['Start Date'] === date.format('DD/MM/YYYY')) - resolve(items) - }) - .on('error', () => { - resolve([]) - }) - }) -} +const stream = require('stream') +const csv = require('csv-parser') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'rotana.net', + days: 2, + url({ channel }) { + return `https://rotana.net/triAssets/uploads/2020/11/${channel.site_id}.csv` + }, + request: { + method: 'POST' + }, + parser: async function ({ buffer, date }) { + let programs = [] + const items = await parseItems(buffer, date) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title: item['Arabic Event Name'], + category: item['Genre'], + description: item['Arabic Extended Description'], + start: start.toJSON(), + stop: stop.toJSON() + }) + }) + + return programs + } +} + +function parseStart(item) { + const time = `${item['Start Date']} ${item['Start Time']}` + + return dayjs.utc(time, 'DD/MM/YYYY HH:mm:ss:00') +} + +function parseStop(item) { + const time = `${item['End Date']} ${item['End Time']}` + + return dayjs.utc(time, 'DD/MM/YYYY HH:mm:ss:00') +} + +function parseItems(buffer, date) { + return new Promise(resolve => { + let items = [] + const input = new stream.PassThrough() + input.end(buffer) + input + .pipe(csv()) + .on('data', data => items.push(data)) + .on('end', () => { + items = items.filter(i => i['Start Date'] === date.format('DD/MM/YYYY')) + resolve(items) + }) + .on('error', () => { + resolve([]) + }) + }) +} diff --git a/sites/rotana.net/rotana.net.test.js b/sites/rotana.net/rotana.net.test.js index a10fe57ff..a006b3837 100644 --- a/sites/rotana.net/rotana.net.test.js +++ b/sites/rotana.net/rotana.net.test.js @@ -1,57 +1,57 @@ -// npm run grab -- --site=rotana.net - -const { parser, url } = require('./rotana.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'KHALIJIA-7', - xmltv_id: 'RotanaKhalejia.sa' -} -const buffer = - Buffer.from(`Event ID,Event Name,Arabic Event Name,Start Date,Start Time,End Date,End Time,Short Description,Arabic Short Description,Extended Description,Arabic Extended Description,,Genre,Audio,Video -,حسب الظروف,حسب الظروف بدون تترات - Episode 16,07/11/2021,23:30:00:00,08/11/2021,00:00:00:00,,,,,,Drama,, -,كورة,كورة,08/11/2021,01:30:00:00,08/11/2021,03:00:00:00,,,,,,Generic,,`) - -it('can generate valid url', () => { - const result = url({ channel, date }) - expect(result).toBe('https://rotana.net/triAssets/uploads/2020/11/KHALIJIA-7.csv') -}) - -it('can parse response', done => { - parser({ date, channel, buffer }) - .then(result => { - expect(result).toMatchObject([ - { - start: '2021-11-08T01:30:00.000Z', - stop: '2021-11-08T03:00:00.000Z', - title: 'كورة', - category: 'Generic', - description: '' - } - ]) - done() - }) - .catch(() => { - done() - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - buffer: Buffer.from('') - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(() => { - done() - }) -}) +// npm run grab -- --site=rotana.net + +const { parser, url } = require('./rotana.net.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'KHALIJIA-7', + xmltv_id: 'RotanaKhalejia.sa' +} +const buffer = + Buffer.from(`Event ID,Event Name,Arabic Event Name,Start Date,Start Time,End Date,End Time,Short Description,Arabic Short Description,Extended Description,Arabic Extended Description,,Genre,Audio,Video +,حسب الظروف,حسب الظروف بدون تترات - Episode 16,07/11/2021,23:30:00:00,08/11/2021,00:00:00:00,,,,,,Drama,, +,كورة,كورة,08/11/2021,01:30:00:00,08/11/2021,03:00:00:00,,,,,,Generic,,`) + +it('can generate valid url', () => { + const result = url({ channel, date }) + expect(result).toBe('https://rotana.net/triAssets/uploads/2020/11/KHALIJIA-7.csv') +}) + +it('can parse response', done => { + parser({ date, channel, buffer }) + .then(result => { + expect(result).toMatchObject([ + { + start: '2021-11-08T01:30:00.000Z', + stop: '2021-11-08T03:00:00.000Z', + title: 'كورة', + category: 'Generic', + description: '' + } + ]) + done() + }) + .catch(() => { + done() + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + buffer: Buffer.from('') + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(() => { + done() + }) +}) diff --git a/sites/rtb.gov.bn/rtb.gov.bn.config.js b/sites/rtb.gov.bn/rtb.gov.bn.config.js index 306ceef81..3af095de4 100644 --- a/sites/rtb.gov.bn/rtb.gov.bn.config.js +++ b/sites/rtb.gov.bn/rtb.gov.bn.config.js @@ -1,74 +1,74 @@ -const pdf = require('pdf-parse') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'rtb.gov.bn', - days: 2, - url: function ({ channel, date }) { - return encodeURI( - `http://www.rtb.gov.bn/PublishingImages/SitePages/Programme Guide/${ - channel.site_id - } ${date.format('DD MMMM YYYY')}.pdf` - ) - }, - parser: async function ({ buffer, date }) { - let programs = [] - const items = await parseItems(buffer) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Brunei') -} - -async function parseItems(buffer) { - let data - try { - data = await pdf(buffer) - } catch (err) { - return [] - } - - if (!data) return [] - - return data.text - .split('\n') - .filter(s => { - const string = s.trim() - - return string && /^\d{2}:\d{2}/.test(string) - }) - .map(s => { - const [, time, title] = s.trim().match(/^(\d{2}:\d{2}) (.*)/) || [null, null, null] - - return { time, title } - }) -} +const pdf = require('pdf-parse') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'rtb.gov.bn', + days: 2, + url: function ({ channel, date }) { + return encodeURI( + `http://www.rtb.gov.bn/PublishingImages/SitePages/Programme Guide/${ + channel.site_id + } ${date.format('DD MMMM YYYY')}.pdf` + ) + }, + parser: async function ({ buffer, date }) { + let programs = [] + const items = await parseItems(buffer) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Brunei') +} + +async function parseItems(buffer) { + let data + try { + data = await pdf(buffer) + } catch (err) { + return [] + } + + if (!data) return [] + + return data.text + .split('\n') + .filter(s => { + const string = s.trim() + + return string && /^\d{2}:\d{2}/.test(string) + }) + .map(s => { + const [, time, title] = s.trim().match(/^(\d{2}:\d{2}) (.*)/) || [null, null, null] + + return { time, title } + }) +} diff --git a/sites/rtb.gov.bn/rtb.gov.bn.test.js b/sites/rtb.gov.bn/rtb.gov.bn.test.js index b3f1ca75e..d539504af 100644 --- a/sites/rtb.gov.bn/rtb.gov.bn.test.js +++ b/sites/rtb.gov.bn/rtb.gov.bn.test.js @@ -1,96 +1,96 @@ -// npm run grab -- --site=rtb.gov.bn - -const { parser, url } = require('./rtb.gov.bn.config.js') -const path = require('path') -const fs = require('fs') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Sukmaindera', - xmltv_id: 'RTBSukmaindera.bn' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'http://www.rtb.gov.bn/PublishingImages/SitePages/Programme%20Guide/Sukmaindera%2011%20November%202021.pdf' - ) -}) - -it('can parse Sukmaindera 11 November 2021.pdf', done => { - const buffer = fs.readFileSync( - path.resolve(__dirname, '__data__/Sukmaindera 11 November 2021.pdf'), - { - charset: 'utf8' - } - ) - parser({ buffer, date }) - .then(results => { - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results.length).toBe(47) - expect(results[0]).toMatchObject({ - start: '2021-11-10T22:00:00.000Z', - stop: '2021-11-10T22:05:00.000Z', - title: 'NATIONAL ANTHEM' - }) - expect(results[46]).toMatchObject({ - start: '2021-11-11T21:30:00.000Z', - stop: '2021-11-11T22:30:00.000Z', - title: 'BACAAN SURAH YASSIN' - }) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can parse Aneka 11 November 2021.pdf', done => { - const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/Aneka 11 November 2021.pdf'), { - charset: 'utf8' - }) - parser({ buffer, date }) - .then(results => { - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results.length).toBe(26) - expect(results[4]).toMatchObject({ - start: '2021-11-11T03:00:00.000Z', - stop: '2021-11-11T04:05:00.000Z', - title: 'DRAMA TURKI:' - }) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: `Object moved -

    Object moved to here.

    - -` - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(error => { - done(error) - }) -}) +// npm run grab -- --site=rtb.gov.bn + +const { parser, url } = require('./rtb.gov.bn.config.js') +const path = require('path') +const fs = require('fs') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Sukmaindera', + xmltv_id: 'RTBSukmaindera.bn' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'http://www.rtb.gov.bn/PublishingImages/SitePages/Programme%20Guide/Sukmaindera%2011%20November%202021.pdf' + ) +}) + +it('can parse Sukmaindera 11 November 2021.pdf', done => { + const buffer = fs.readFileSync( + path.resolve(__dirname, '__data__/Sukmaindera 11 November 2021.pdf'), + { + charset: 'utf8' + } + ) + parser({ buffer, date }) + .then(results => { + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results.length).toBe(47) + expect(results[0]).toMatchObject({ + start: '2021-11-10T22:00:00.000Z', + stop: '2021-11-10T22:05:00.000Z', + title: 'NATIONAL ANTHEM' + }) + expect(results[46]).toMatchObject({ + start: '2021-11-11T21:30:00.000Z', + stop: '2021-11-11T22:30:00.000Z', + title: 'BACAAN SURAH YASSIN' + }) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can parse Aneka 11 November 2021.pdf', done => { + const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/Aneka 11 November 2021.pdf'), { + charset: 'utf8' + }) + parser({ buffer, date }) + .then(results => { + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results.length).toBe(26) + expect(results[4]).toMatchObject({ + start: '2021-11-11T03:00:00.000Z', + stop: '2021-11-11T04:05:00.000Z', + title: 'DRAMA TURKI:' + }) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: `Object moved +

    Object moved to here.

    + +` + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(error => { + done(error) + }) +}) diff --git a/sites/rthk.hk/rthk.hk.config.js b/sites/rthk.hk/rthk.hk.config.js index 76f9987e4..cf500571e 100644 --- a/sites/rthk.hk/rthk.hk.config.js +++ b/sites/rthk.hk/rthk.hk.config.js @@ -1,89 +1,89 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'rthk.hk', - days: 2, - request: { - headers({ channel }) { - return { - Cookie: `lang=${channel.lang}` - } - }, - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ date }) { - return `https://www.rthk.hk/timetable/main_timetable/${date.format('YYYYMMDD')}` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const $item = cheerio.load(item) - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - categories: parseCategories($item), - icon: parseIcon($item), - start: parseStart($item, date), - stop: parseStop($item, date) - }) - } - - return programs - } -} - -function parseIcon($item) { - return $item('.single-wrap').data('p') -} - -function parseCategories($item) { - let cate = $item('.single-wrap').data('cate') || '' - let [, categories] = cate.match(/^\|(.*)\|$/) || [null, ''] - - return categories.split('||').filter(Boolean) -} - -function parseTitle($item) { - return $item('.showTit').attr('title') -} - -function parseSubTitle($item) { - return $item('.showEpi').attr('title') -} - -function parseStart($item, date) { - const timeRow = $item('.timeRow').text().trim() - const [, HH, mm] = timeRow.match(/^(\d+):(\d+)-/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') -} - -function parseStop($item, date) { - const timeRow = $item('.timeRow').text().trim() - const [, HH, mm] = timeRow.match(/-(\d+):(\d+)$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.result)) return [] - const channelData = data.result.find(i => i.key == channel.site_id) - if (!channelData || !channelData.data) return [] - const $ = cheerio.load(channelData.data) - - return $('.showWrap').toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'rthk.hk', + days: 2, + request: { + headers({ channel }) { + return { + Cookie: `lang=${channel.lang}` + } + }, + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ date }) { + return `https://www.rthk.hk/timetable/main_timetable/${date.format('YYYYMMDD')}` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const $item = cheerio.load(item) + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + categories: parseCategories($item), + icon: parseIcon($item), + start: parseStart($item, date), + stop: parseStop($item, date) + }) + } + + return programs + } +} + +function parseIcon($item) { + return $item('.single-wrap').data('p') +} + +function parseCategories($item) { + let cate = $item('.single-wrap').data('cate') || '' + let [, categories] = cate.match(/^\|(.*)\|$/) || [null, ''] + + return categories.split('||').filter(Boolean) +} + +function parseTitle($item) { + return $item('.showTit').attr('title') +} + +function parseSubTitle($item) { + return $item('.showEpi').attr('title') +} + +function parseStart($item, date) { + const timeRow = $item('.timeRow').text().trim() + const [, HH, mm] = timeRow.match(/^(\d+):(\d+)-/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') +} + +function parseStop($item, date) { + const timeRow = $item('.timeRow').text().trim() + const [, HH, mm] = timeRow.match(/-(\d+):(\d+)$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.result)) return [] + const channelData = data.result.find(i => i.key == channel.site_id) + if (!channelData || !channelData.data) return [] + const $ = cheerio.load(channelData.data) + + return $('.showWrap').toArray() +} diff --git a/sites/rthk.hk/rthk.hk.test.js b/sites/rthk.hk/rthk.hk.test.js index 2917f1eb7..b11edc36f 100644 --- a/sites/rthk.hk/rthk.hk.test.js +++ b/sites/rthk.hk/rthk.hk.test.js @@ -1,82 +1,82 @@ -// npm run grab -- --site=rthk.hk - -const { parser, url, request } = require('./rthk.hk.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '31', - xmltv_id: 'RTHKTV31.hk', - lang: 'zh' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.rthk.hk/timetable/main_timetable/20221202') -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - Cookie: 'lang=zh' - }) -}) - -it('can generate valid request headers for English version', () => { - const channelEN = { ...channel, lang: 'en' } - - expect(request.headers({ channel: channelEN })).toMatchObject({ - Cookie: 'lang=en' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zh.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T16:00:00.000Z', - stop: '2022-12-01T17:00:00.000Z', - title: '問天', - sub_title: '第十四集', - categories: ['戲劇'], - icon: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' - }) -}) - -it('can parse response in English', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T16:00:00.000Z', - stop: '2022-12-01T17:00:00.000Z', - title: 'The Great Aerospace', - sub_title: 'Episode 14', - categories: ['戲劇'], - icon: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ date, channel, content }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=rthk.hk + +const { parser, url, request } = require('./rthk.hk.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '31', + xmltv_id: 'RTHKTV31.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.rthk.hk/timetable/main_timetable/20221202') +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + Cookie: 'lang=zh' + }) +}) + +it('can generate valid request headers for English version', () => { + const channelEN = { ...channel, lang: 'en' } + + expect(request.headers({ channel: channelEN })).toMatchObject({ + Cookie: 'lang=en' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zh.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T16:00:00.000Z', + stop: '2022-12-01T17:00:00.000Z', + title: '問天', + sub_title: '第十四集', + categories: ['戲劇'], + icon: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' + }) +}) + +it('can parse response in English', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T16:00:00.000Z', + stop: '2022-12-01T17:00:00.000Z', + title: 'The Great Aerospace', + sub_title: 'Episode 14', + categories: ['戲劇'], + icon: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ date, channel, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js index d3cc83b6b..2f68089d7 100644 --- a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js +++ b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'rtmklik.rtm.gov.my', - days: 2, - url: function ({ date, channel }) { - return `https://rtm.glueapi.io/v3/epg/${ - channel.site_id - }/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format( - 'YYYY-MM-DD' - )}&timezone=0` - // return `https://rtm.glueapi.io/v3/epg/99/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format('YYYY-MM-DD')}&timezone=0` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.programTitle, - description: item.description, - start: parseTime(item.dateTimeStart), - stop: parseTime(item.dateTimeEnd) - }) - }) - - return programs - } -} - -function parseItems(content) { - const data = JSON.parse(content) - return data.schedule ? data.schedule : [] -} - -function parseTime(time) { - return dayjs.utc(time, 'YYYY-MM-DDTHH:mm:ss') -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'rtmklik.rtm.gov.my', + days: 2, + url: function ({ date, channel }) { + return `https://rtm.glueapi.io/v3/epg/${ + channel.site_id + }/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format( + 'YYYY-MM-DD' + )}&timezone=0` + // return `https://rtm.glueapi.io/v3/epg/99/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format('YYYY-MM-DD')}&timezone=0` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.programTitle, + description: item.description, + start: parseTime(item.dateTimeStart), + stop: parseTime(item.dateTimeEnd) + }) + }) + + return programs + } +} + +function parseItems(content) { + const data = JSON.parse(content) + return data.schedule ? data.schedule : [] +} + +function parseTime(time) { + return dayjs.utc(time, 'YYYY-MM-DDTHH:mm:ss') +} diff --git a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js index 23c8a1816..23f7b2803 100644 --- a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js +++ b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=rtmklik.rtm.gov.my - -const { parser, url } = require('./rtmklik.rtm.gov.my.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-09-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'TV2.my' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0' - ) -}) - -it('can parse response', () => { - const content = - '{"id":2,"channel":"TV2","channelId":"2","image":"/live_channel/tv2_Trans.png","idAuthor":9,"type":"TV","timezone":8,"dateCreated":"2022-07-08T01:22:33.233","dateModified":"2022-07-21T21:58:39.77","itemCount":30,"prev":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-03&dateEnd=2022-09-03","next":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-05&dateEnd=2022-09-05","schedule":[{"idEPGProgramSchedule":109303,"dateTimeStart":"2022-09-04T19:00:00","dateTimeEnd":"2022-09-04T20:00:00","scheduleProgramTitle":"Hope Of Life","scheduleProgramDescription":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","scheduleEpisodeNumber":0,"scheduleSeries":0,"duration":3600,"idEPGProgram":3603,"programTitle":"Hope Of Life","description":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","episodeNumber":0,"series":0,"repeat":"Never","dateModified":"2022-08-29T02:14:56.647","dateCreated":"0001-01-01T00:00:00"}]}' - - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-09-04T19:00:00.000Z', - stop: '2022-09-04T20:00:00.000Z', - title: 'Hope Of Life', - description: - 'Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"id":2,"channel":"TV2","channelId":"2","schedule":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=rtmklik.rtm.gov.my + +const { parser, url } = require('./rtmklik.rtm.gov.my.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-09-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'TV2.my' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0' + ) +}) + +it('can parse response', () => { + const content = + '{"id":2,"channel":"TV2","channelId":"2","image":"/live_channel/tv2_Trans.png","idAuthor":9,"type":"TV","timezone":8,"dateCreated":"2022-07-08T01:22:33.233","dateModified":"2022-07-21T21:58:39.77","itemCount":30,"prev":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-03&dateEnd=2022-09-03","next":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-05&dateEnd=2022-09-05","schedule":[{"idEPGProgramSchedule":109303,"dateTimeStart":"2022-09-04T19:00:00","dateTimeEnd":"2022-09-04T20:00:00","scheduleProgramTitle":"Hope Of Life","scheduleProgramDescription":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","scheduleEpisodeNumber":0,"scheduleSeries":0,"duration":3600,"idEPGProgram":3603,"programTitle":"Hope Of Life","description":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","episodeNumber":0,"series":0,"repeat":"Never","dateModified":"2022-08-29T02:14:56.647","dateCreated":"0001-01-01T00:00:00"}]}' + + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-09-04T19:00:00.000Z', + stop: '2022-09-04T20:00:00.000Z', + title: 'Hope Of Life', + description: + 'Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"id":2,"channel":"TV2","channelId":"2","schedule":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/rtp.pt/rtp.pt.config.js b/sites/rtp.pt/rtp.pt.config.js index 12e552cf0..a8d004416 100644 --- a/sites/rtp.pt/rtp.pt.config.js +++ b/sites/rtp.pt/rtp.pt.config.js @@ -1,81 +1,81 @@ -const _ = require('lodash') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const tz = { - lis: 'Europe/Lisbon', - per: 'Asia/Macau', - rja: 'America/Sao_Paulo' -} - -module.exports = { - site: 'rtp.pt', - days: 2, - url({ channel, date }) { - let [region, channelCode] = channel.site_id.split('#') - return `https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/${channelCode}/${date.format( - 'D-M-YYYY' - )}/${region}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, channel) - if (!start) return - if (prev) { - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: item.name, - description: item.description, - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://www.rtp.pt/EPG/json/rtp-home-page/list-channels/tv') - .then(r => r.data.result) - .catch(console.error) - - return items.map(i => { - return { - lang: 'pt', - site_id: i.channel_code, - name: i.channel_name - } - }) - } -} - -function parseIcon(item) { - const last = item.image.pop() - if (!last) return null - return last.src -} - -function parseStart(item, channel) { - let [region] = channel.site_id.split('#') - return dayjs.tz(item.date, 'YYYY-MM-DD HH:mm:ss', tz[region]) -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - - return _.flatten(Object.values(data.result)) -} +const _ = require('lodash') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const tz = { + lis: 'Europe/Lisbon', + per: 'Asia/Macau', + rja: 'America/Sao_Paulo' +} + +module.exports = { + site: 'rtp.pt', + days: 2, + url({ channel, date }) { + let [region, channelCode] = channel.site_id.split('#') + return `https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/${channelCode}/${date.format( + 'D-M-YYYY' + )}/${region}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, channel) + if (!start) return + if (prev) { + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: item.name, + description: item.description, + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://www.rtp.pt/EPG/json/rtp-home-page/list-channels/tv') + .then(r => r.data.result) + .catch(console.error) + + return items.map(i => { + return { + lang: 'pt', + site_id: i.channel_code, + name: i.channel_name + } + }) + } +} + +function parseIcon(item) { + const last = item.image.pop() + if (!last) return null + return last.src +} + +function parseStart(item, channel) { + let [region] = channel.site_id.split('#') + return dayjs.tz(item.date, 'YYYY-MM-DD HH:mm:ss', tz[region]) +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + + return _.flatten(Object.values(data.result)) +} diff --git a/sites/rtp.pt/rtp.pt.test.js b/sites/rtp.pt/rtp.pt.test.js index 6b1f98261..fd25ba3ca 100644 --- a/sites/rtp.pt/rtp.pt.test.js +++ b/sites/rtp.pt/rtp.pt.test.js @@ -1,45 +1,45 @@ -// npm run channels:parse -- --config=./sites/rtp.pt/rtp.pt.config.js --output=./sites/rtp.pt/rtp.pt.channels.xml -// npm run grab -- --site=rtp.pt - -const { parser, url } = require('./rtp.pt.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lis#4', - xmltv_id: 'RTPMadeira.pt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/4/2-12-2022/lis' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[9]).toMatchObject({ - start: '2022-12-02T23:30:00.000Z', - stop: '2022-12-03T00:00:00.000Z', - title: 'Telejornal Madeira', - description: 'Informação de proximidade. De confiança!', - icon: 'https://cdn-images.rtp.pt/EPG/imagens/15790_43438_8820.png?w=384&h=216' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/rtp.pt/rtp.pt.config.js --output=./sites/rtp.pt/rtp.pt.channels.xml +// npm run grab -- --site=rtp.pt + +const { parser, url } = require('./rtp.pt.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lis#4', + xmltv_id: 'RTPMadeira.pt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/4/2-12-2022/lis' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[9]).toMatchObject({ + start: '2022-12-02T23:30:00.000Z', + stop: '2022-12-03T00:00:00.000Z', + title: 'Telejornal Madeira', + description: 'Informação de proximidade. De confiança!', + icon: 'https://cdn-images.rtp.pt/EPG/imagens/15790_43438_8820.png?w=384&h=216' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ruv.is/ruv.is.config.js b/sites/ruv.is/ruv.is.config.js index 355fd9dd0..9406ab26f 100644 --- a/sites/ruv.is/ruv.is.config.js +++ b/sites/ruv.is/ruv.is.config.js @@ -1,79 +1,79 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'ruv.is', - days: 2, - url({ channel, date }) { - let params = new URLSearchParams() - params.append('operationName', 'getSchedule') - params.append( - 'variables', - JSON.stringify({ channel: channel.site_id, date: date.format('YYYY-MM-DD') }) - ) - params.append( - 'extensions', - JSON.stringify({ - persistedQuery: { - version: 1, - sha256Hash: '7d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6' - } - }) - ) - - return `https://www.ruv.is/gql/?${params.toString()}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - let start = parseStart(item, date) - let stop = parseStop(item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - } - programs.push({ - title: item.title, - description: item.description, - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.image.replace('$$IMAGESIZE$$', '480') -} - -function parseStart(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.start_time_friendly}`, - 'YYYY-MM-DD HH:mm', - 'Atlantic/Reykjavik' - ) -} - -function parseStop(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.end_time_friendly}`, - 'YYYY-MM-DD HH:mm', - 'Atlantic/Reykjavik' - ) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data.Schedule.events)) return [] - - return data.data.Schedule.events -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'ruv.is', + days: 2, + url({ channel, date }) { + let params = new URLSearchParams() + params.append('operationName', 'getSchedule') + params.append( + 'variables', + JSON.stringify({ channel: channel.site_id, date: date.format('YYYY-MM-DD') }) + ) + params.append( + 'extensions', + JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: '7d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6' + } + }) + ) + + return `https://www.ruv.is/gql/?${params.toString()}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + let start = parseStart(item, date) + let stop = parseStop(item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + } + programs.push({ + title: item.title, + description: item.description, + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.image.replace('$$IMAGESIZE$$', '480') +} + +function parseStart(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.start_time_friendly}`, + 'YYYY-MM-DD HH:mm', + 'Atlantic/Reykjavik' + ) +} + +function parseStop(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.end_time_friendly}`, + 'YYYY-MM-DD HH:mm', + 'Atlantic/Reykjavik' + ) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data.Schedule.events)) return [] + + return data.data.Schedule.events +} diff --git a/sites/ruv.is/ruv.is.test.js b/sites/ruv.is/ruv.is.test.js index f034d9483..a415b30a8 100644 --- a/sites/ruv.is/ruv.is.test.js +++ b/sites/ruv.is/ruv.is.test.js @@ -1,48 +1,48 @@ -// npm run grab -- --site=ruv.is - -const { parser, url } = require('./ruv.is.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ruv', - xmltv_id: 'RUV.is' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.ruv.is/gql/?operationName=getSchedule&variables=%7B%22channel%22%3A%22ruv%22%2C%22date%22%3A%222023-01-17%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%227d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6%22%7D%7D' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-17T13:00:00.000Z', - stop: '2023-01-17T13:10:00.000Z', - title: 'Heimaleikfimi', - description: - 'Góð ráð og æfingar sem tilvalið er að gera heima. Íris Rut Garðarsdóttir sjúkraþjálfari hefur umsjón með leikfiminni. e.', - icon: 'https://d38kdhuogyllre.cloudfront.net/fit-in/480x/filters:quality(65)/hd_posters/91pvig-3p3hig.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=ruv.is + +const { parser, url } = require('./ruv.is.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ruv', + xmltv_id: 'RUV.is' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.ruv.is/gql/?operationName=getSchedule&variables=%7B%22channel%22%3A%22ruv%22%2C%22date%22%3A%222023-01-17%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%227d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6%22%7D%7D' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-17T13:00:00.000Z', + stop: '2023-01-17T13:10:00.000Z', + title: 'Heimaleikfimi', + description: + 'Góð ráð og æfingar sem tilvalið er að gera heima. Íris Rut Garðarsdóttir sjúkraþjálfari hefur umsjón með leikfiminni. e.', + icon: 'https://d38kdhuogyllre.cloudfront.net/fit-in/480x/filters:quality(65)/hd_posters/91pvig-3p3hig.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sat.tv/sat.tv.config.js b/sites/sat.tv/sat.tv.config.js index e3dc429d7..0fc8e652d 100644 --- a/sites/sat.tv/sat.tv.config.js +++ b/sites/sat.tv/sat.tv.config.js @@ -1,129 +1,129 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php' - -module.exports = { - site: 'sat.tv', - days: 2, - url: API_ENDPOINT, - request: { - method: 'POST', - headers({ channel }) { - return { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: `pll_language=${channel.lang}` - } - }, - data({ channel, date }) { - const [satSatellite, satLineup] = channel.site_id.split('#') - const params = new URLSearchParams() - params.append('dateFiltre', date.format('YYYY-MM-DD')) - params.append('hoursFiltre', '0') - params.append('satLineup', satLineup) - params.append('satSatellite', satSatellite) - params.append('userDateTime', date.valueOf()) - params.append('userTimezone', 'Europe/London') - - return params - } - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - let $item = cheerio.load(item) - let start = parseStart($item, date) - let duration = parseDuration($item) - let stop = start.add(duration, 'm') - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels({ lang, satLineup, satSatellite }) { - const params = new URLSearchParams() - params.append('dateFiltre', dayjs().format('YYYY-MM-DD')) - params.append('hoursFiltre', '0') - params.append('satLineup', satLineup) - params.append('satSatellite', satSatellite) - params.append('userDateTime', dayjs().valueOf()) - params.append('userTimezone', 'Europe/London') - const data = await axios - .post(API_ENDPOINT, params, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: `pll_language=${lang}` - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - const channels = $('.main-container-channels-events > .container-channel-events').toArray() - - return channels.map(item => { - const name = $(item).find('.channel-title').text().trim() - return { - lang, - site_id: name, - name - } - }) - } -} - -function parseIcon($item) { - const src = $item('.event-logo img:not(.no-img)').attr('src') - - return src ? `https://sat.tv${src}` : null -} - -function parseTitle($item) { - return $item('.event-data-title').text() -} - -function parseDescription($item) { - return $item('.event-data-desc').text() -} - -function parseStart($item, date) { - let eventDataDate = $item('.event-data-date').text().trim() - let [, time] = eventDataDate.match(/(\d{2}:\d{2})/) || [null, null] - if (!time) return null - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseDuration($item) { - let eventDataInfo = $item('.event-data-info').text().trim() - let [, h, m] = eventDataInfo.match(/(\d{2})h(\d{2})/) || [null, 0, 0] - - return parseInt(h) * 60 + parseInt(m) -} - -function parseItems(content, channel) { - const [, , site_id] = channel.site_id.split('#') - const $ = cheerio.load(content) - const channelData = $('.main-container-channels-events > .container-channel-events') - .filter((index, el) => { - return $(el).find('.channel-title').text().trim() === site_id - }) - .first() - if (!channelData) return [] - - return $(channelData).find('.container-event').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php' + +module.exports = { + site: 'sat.tv', + days: 2, + url: API_ENDPOINT, + request: { + method: 'POST', + headers({ channel }) { + return { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: `pll_language=${channel.lang}` + } + }, + data({ channel, date }) { + const [satSatellite, satLineup] = channel.site_id.split('#') + const params = new URLSearchParams() + params.append('dateFiltre', date.format('YYYY-MM-DD')) + params.append('hoursFiltre', '0') + params.append('satLineup', satLineup) + params.append('satSatellite', satSatellite) + params.append('userDateTime', date.valueOf()) + params.append('userTimezone', 'Europe/London') + + return params + } + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + let $item = cheerio.load(item) + let start = parseStart($item, date) + let duration = parseDuration($item) + let stop = start.add(duration, 'm') + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels({ lang, satLineup, satSatellite }) { + const params = new URLSearchParams() + params.append('dateFiltre', dayjs().format('YYYY-MM-DD')) + params.append('hoursFiltre', '0') + params.append('satLineup', satLineup) + params.append('satSatellite', satSatellite) + params.append('userDateTime', dayjs().valueOf()) + params.append('userTimezone', 'Europe/London') + const data = await axios + .post(API_ENDPOINT, params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: `pll_language=${lang}` + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + const channels = $('.main-container-channels-events > .container-channel-events').toArray() + + return channels.map(item => { + const name = $(item).find('.channel-title').text().trim() + return { + lang, + site_id: name, + name + } + }) + } +} + +function parseIcon($item) { + const src = $item('.event-logo img:not(.no-img)').attr('src') + + return src ? `https://sat.tv${src}` : null +} + +function parseTitle($item) { + return $item('.event-data-title').text() +} + +function parseDescription($item) { + return $item('.event-data-desc').text() +} + +function parseStart($item, date) { + let eventDataDate = $item('.event-data-date').text().trim() + let [, time] = eventDataDate.match(/(\d{2}:\d{2})/) || [null, null] + if (!time) return null + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseDuration($item) { + let eventDataInfo = $item('.event-data-info').text().trim() + let [, h, m] = eventDataInfo.match(/(\d{2})h(\d{2})/) || [null, 0, 0] + + return parseInt(h) * 60 + parseInt(m) +} + +function parseItems(content, channel) { + const [, , site_id] = channel.site_id.split('#') + const $ = cheerio.load(content) + const channelData = $('.main-container-channels-events > .container-channel-events') + .filter((index, el) => { + return $(el).find('.channel-title').text().trim() === site_id + }) + .first() + if (!channelData) return [] + + return $(channelData).find('.container-event').toArray() +} diff --git a/sites/sat.tv/sat.tv.test.js b/sites/sat.tv/sat.tv.test.js index ee30ef072..2a7769b04 100644 --- a/sites/sat.tv/sat.tv.test.js +++ b/sites/sat.tv/sat.tv.test.js @@ -1,112 +1,112 @@ -// npm run channels:parse -- --config=sites/sat.tv/sat.tv.config.js --output=sites/sat.tv/sat.tv.channels.xml --set=lang:ar --set=satSatellite:1 --set=satLineup:38 -// npm run grab -- --site=sat.tv - -const { parser, url, request } = require('./sat.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1#38#السعودية', - xmltv_id: 'AlSaudiya.sa', - lang: 'ar' -} - -it('can generate valid url', () => { - expect(url).toBe('https://sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: 'pll_language=ar' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ channel, date }) - expect(data.get('dateFiltre')).toBe('2023-06-26') - expect(data.get('hoursFiltre')).toBe('0') - expect(data.get('satLineup')).toBe('38') - expect(data.get('satSatellite')).toBe('1') - expect(data.get('userDateTime')).toBe('1687737600000') - expect(data.get('userTimezone')).toBe('Europe/London') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ar.html')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(35) - expect(results[0]).toMatchObject({ - start: '2023-06-26T06:30:00.000Z', - stop: '2023-06-26T07:00:00.000Z', - title: 'تعظيم البلد الحرام', - description: `الناس, دين, ثقافة -يلقي صانع الفيلم الضوء على مشروع تعظيم البلد الحرام في مكة من العائلة الملكية في المملكة العربية السعودية، والذي يهدف لإبراز حرمته لدى المسلمين حول العالم.`, - icon: null - }) - - expect(results[34]).toMatchObject({ - start: '2023-06-26T22:30:00.000Z', - stop: '2023-06-27T01:00:00.000Z', - title: 'الأخبار', - description: `نشرة -.يطرح أهم القضايا والأحداث على الساحة السعودية والعالمية`, - icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' - }) -}) - -it('can parse response in english', () => { - const channel = { - site_id: '1#38#Saudi HD', - xmltv_id: 'AlSaudiya.sa', - lang: 'en' - } - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.html')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(32) - expect(results[0]).toMatchObject({ - start: '2023-06-26T09:00:00.000Z', - stop: '2023-06-26T10:00:00.000Z', - title: 'News', - description: `Newscast -The most important issues and events on the Saudi and the world.`, - icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' - }) - - expect(results[31]).toMatchObject({ - start: '2023-06-26T23:15:00.000Z', - stop: '2023-06-27T00:00:00.000Z', - title: "Bride's Father", - description: `Romance, Drama, Family -2022 -Abdelhamid's family struggles to deal with the challenges of life that keep flowing one by one. they manage to stay strong-armed with their love and trust for each other. -Sayed Ragab, Sawsan Badr, Medhat Saleh, Nermine Al Feqy, Mohamed Adel, Khaled Kamal, Rania Farid, Hani Kamal, Hani Kamal`, - icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3157177.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, date, channel }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/sat.tv/sat.tv.config.js --output=sites/sat.tv/sat.tv.channels.xml --set=lang:ar --set=satSatellite:1 --set=satLineup:38 +// npm run grab -- --site=sat.tv + +const { parser, url, request } = require('./sat.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1#38#السعودية', + xmltv_id: 'AlSaudiya.sa', + lang: 'ar' +} + +it('can generate valid url', () => { + expect(url).toBe('https://sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: 'pll_language=ar' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ channel, date }) + expect(data.get('dateFiltre')).toBe('2023-06-26') + expect(data.get('hoursFiltre')).toBe('0') + expect(data.get('satLineup')).toBe('38') + expect(data.get('satSatellite')).toBe('1') + expect(data.get('userDateTime')).toBe('1687737600000') + expect(data.get('userTimezone')).toBe('Europe/London') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ar.html')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(35) + expect(results[0]).toMatchObject({ + start: '2023-06-26T06:30:00.000Z', + stop: '2023-06-26T07:00:00.000Z', + title: 'تعظيم البلد الحرام', + description: `الناس, دين, ثقافة +يلقي صانع الفيلم الضوء على مشروع تعظيم البلد الحرام في مكة من العائلة الملكية في المملكة العربية السعودية، والذي يهدف لإبراز حرمته لدى المسلمين حول العالم.`, + icon: null + }) + + expect(results[34]).toMatchObject({ + start: '2023-06-26T22:30:00.000Z', + stop: '2023-06-27T01:00:00.000Z', + title: 'الأخبار', + description: `نشرة +.يطرح أهم القضايا والأحداث على الساحة السعودية والعالمية`, + icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' + }) +}) + +it('can parse response in english', () => { + const channel = { + site_id: '1#38#Saudi HD', + xmltv_id: 'AlSaudiya.sa', + lang: 'en' + } + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.html')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(32) + expect(results[0]).toMatchObject({ + start: '2023-06-26T09:00:00.000Z', + stop: '2023-06-26T10:00:00.000Z', + title: 'News', + description: `Newscast +The most important issues and events on the Saudi and the world.`, + icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' + }) + + expect(results[31]).toMatchObject({ + start: '2023-06-26T23:15:00.000Z', + stop: '2023-06-27T00:00:00.000Z', + title: "Bride's Father", + description: `Romance, Drama, Family +2022 +Abdelhamid's family struggles to deal with the challenges of life that keep flowing one by one. they manage to stay strong-armed with their love and trust for each other. +Sayed Ragab, Sawsan Badr, Medhat Saleh, Nermine Al Feqy, Mohamed Adel, Khaled Kamal, Rania Farid, Hani Kamal, Hani Kamal`, + icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3157177.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, date, channel }) + expect(result).toMatchObject([]) +}) diff --git a/sites/siba.com.co/siba.com.co.config.js b/sites/siba.com.co/siba.com.co.config.js index d1ef37d20..970815bee 100644 --- a/sites/siba.com.co/siba.com.co.config.js +++ b/sites/siba.com.co/siba.com.co.config.js @@ -1,56 +1,56 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'siba.com.co', - days: 2, - url: 'http://devportal.siba.com.co/index.php?action=grilla', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('servicio', '10') - params.append('ini', date.unix()) - params.append('end', date.add(1, 'd').unix()) - params.append('chn', channel.site_id) - - return params - } - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.nom, - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.unix(item.ini) -} - -function parseStop(item) { - return dayjs.unix(item.fin) -} - -function parseContent(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.list)) return null - - return data.list.find(i => i.id === channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - - return data ? data.prog : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'siba.com.co', + days: 2, + url: 'http://devportal.siba.com.co/index.php?action=grilla', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('servicio', '10') + params.append('ini', date.unix()) + params.append('end', date.add(1, 'd').unix()) + params.append('chn', channel.site_id) + + return params + } + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.nom, + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.unix(item.ini) +} + +function parseStop(item) { + return dayjs.unix(item.fin) +} + +function parseContent(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.list)) return null + + return data.list.find(i => i.id === channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + + return data ? data.prog : [] +} diff --git a/sites/siba.com.co/siba.com.co.test.js b/sites/siba.com.co/siba.com.co.test.js index a466ded67..c5f7bc26c 100644 --- a/sites/siba.com.co/siba.com.co.test.js +++ b/sites/siba.com.co/siba.com.co.test.js @@ -1,54 +1,54 @@ -// npm run grab -- --site=siba.com.co - -const { parser, url, request } = require('./siba.com.co.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '395', - xmltv_id: 'CanalClaro.cl' -} -const content = - '{"list":[{"id":"395","nom":"CANAL CLARO","num":"102","logo":"7c4b9e8566a6e867d1db4c7ce845f1f4.jpg","cat":"Exclusivos Claro","prog":[{"id":"665724465","nom":"Worst Cooks In America","ini":1636588800,"fin":1636592400}]}],"error":null}' - -it('can generate valid url', () => { - expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.has('servicio')).toBe(true) - expect(result.has('ini')).toBe(true) - expect(result.has('end')).toBe(true) - expect(result.has('chn')).toBe(true) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-11T00:00:00.000Z', - stop: '2021-11-11T01:00:00.000Z', - title: 'Worst Cooks In America' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"list":[],"error":null}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=siba.com.co + +const { parser, url, request } = require('./siba.com.co.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '395', + xmltv_id: 'CanalClaro.cl' +} +const content = + '{"list":[{"id":"395","nom":"CANAL CLARO","num":"102","logo":"7c4b9e8566a6e867d1db4c7ce845f1f4.jpg","cat":"Exclusivos Claro","prog":[{"id":"665724465","nom":"Worst Cooks In America","ini":1636588800,"fin":1636592400}]}],"error":null}' + +it('can generate valid url', () => { + expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.has('servicio')).toBe(true) + expect(result.has('ini')).toBe(true) + expect(result.has('end')).toBe(true) + expect(result.has('chn')).toBe(true) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-11T00:00:00.000Z', + stop: '2021-11-11T01:00:00.000Z', + title: 'Worst Cooks In America' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"list":[],"error":null}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/singtel.com/singtel.com.config.js b/sites/singtel.com/singtel.com.config.js index f15d1b480..51215babd 100644 --- a/sites/singtel.com/singtel.com.config.js +++ b/sites/singtel.com/singtel.com.config.js @@ -1,47 +1,47 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'singtel.com', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/${date.format( - 'DDMMYYYY' - )}.json` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = dayjs.tz(item.startDateTime, 'Asia/Singapore') - const stop = start.add(item.duration, 's') - programs.push({ - title: item.program.title, - category: item.program.subCategory, - description: item.program.description, - start, - stop - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - return data && data[channel.site_id] ? data[channel.site_id] : [] - } catch (err) { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'singtel.com', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/${date.format( + 'DDMMYYYY' + )}.json` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = dayjs.tz(item.startDateTime, 'Asia/Singapore') + const stop = start.add(item.duration, 's') + programs.push({ + title: item.program.title, + category: item.program.subCategory, + description: item.program.description, + start, + stop + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + return data && data[channel.site_id] ? data[channel.site_id] : [] + } catch (err) { + return [] + } +} diff --git a/sites/singtel.com/singtel.com.test.js b/sites/singtel.com/singtel.com.test.js index 9daa2fa8a..f20cf57ef 100644 --- a/sites/singtel.com/singtel.com.test.js +++ b/sites/singtel.com/singtel.com.test.js @@ -1,62 +1,62 @@ -// npm run channels:parse -- --config=./sites/singtel.com/singtel.com.config.js --output=./sites/singtel.com/singtel.com.channels.xml -// npm run grab -- --site=singtel.com - -const { parser, url } = require('./singtel.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5418', - xmltv_id: 'ParamountNetworkSingapore.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/29012023.json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(23) - expect(results[0]).toMatchObject({ - start: '2023-01-28T16:00:00.000Z', - stop: '2023-01-28T17:30:00.000Z', - title: 'Hip Hop Family Christmas Wedding', - description: - 'Hip Hop\'s most famous family is back, and this time Christmas wedding bells are ringing! Jessica and Jayson are getting ready to say their "I do\'s".', - category: 'Specials' - }) - - expect(results[10]).toMatchObject({ - start: '2023-01-29T01:00:00.000Z', - stop: '2023-01-29T01:30:00.000Z', - title: 'The Daily Show', - description: - 'The Daily Show correspondents tackle the biggest stories in news, politics and pop culture.', - category: 'English Entertainment' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const results = parser({ content, channel }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/singtel.com/singtel.com.config.js --output=./sites/singtel.com/singtel.com.channels.xml +// npm run grab -- --site=singtel.com + +const { parser, url } = require('./singtel.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5418', + xmltv_id: 'ParamountNetworkSingapore.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/29012023.json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(23) + expect(results[0]).toMatchObject({ + start: '2023-01-28T16:00:00.000Z', + stop: '2023-01-28T17:30:00.000Z', + title: 'Hip Hop Family Christmas Wedding', + description: + 'Hip Hop\'s most famous family is back, and this time Christmas wedding bells are ringing! Jessica and Jayson are getting ready to say their "I do\'s".', + category: 'Specials' + }) + + expect(results[10]).toMatchObject({ + start: '2023-01-29T01:00:00.000Z', + stop: '2023-01-29T01:30:00.000Z', + title: 'The Daily Show', + description: + 'The Daily Show correspondents tackle the biggest stories in news, politics and pop culture.', + category: 'English Entertainment' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const results = parser({ content, channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/sjonvarp.is/sjonvarp.is.config.js b/sites/sjonvarp.is/sjonvarp.is.config.js index d9b159465..6fac4eb4c 100644 --- a/sites/sjonvarp.is/sjonvarp.is.config.js +++ b/sites/sjonvarp.is/sjonvarp.is.config.js @@ -1,64 +1,64 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'sjonvarp.is', - days: 2, - url: function ({ channel, date }) { - return `http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=${channel.site_id}&y=${date.format( - 'YYYY' - )}&m=${date.format('MM')}&d=${date.format('DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.day-listing-title').text() -} - -function parseDescription($item) { - return $item('.day-listing-description').text() -} - -function parseStart($item, date) { - const time = $item('.day-listing-time') - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - 'body > div.container.nano-container > div > ul > div.day-listing > div:not(.day-listing-channel)' - ).toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'sjonvarp.is', + days: 2, + url: function ({ channel, date }) { + return `http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=${channel.site_id}&y=${date.format( + 'YYYY' + )}&m=${date.format('MM')}&d=${date.format('DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.day-listing-title').text() +} + +function parseDescription($item) { + return $item('.day-listing-description').text() +} + +function parseStart($item, date) { + const time = $item('.day-listing-time') + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + 'body > div.container.nano-container > div > ul > div.day-listing > div:not(.day-listing-channel)' + ).toArray() +} diff --git a/sites/sjonvarp.is/sjonvarp.is.test.js b/sites/sjonvarp.is/sjonvarp.is.test.js index a36f90830..90acbc87a 100644 --- a/sites/sjonvarp.is/sjonvarp.is.test.js +++ b/sites/sjonvarp.is/sjonvarp.is.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=sjonvarp.is - -const { parser, url } = require('./sjonvarp.is.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'RUV', - xmltv_id: 'RUV.is' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=RUV&y=2022&m=08&d=28' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-28T07:15:00.000Z', - stop: '2022-08-28T07:16:00.000Z', - title: 'KrakkaRÚV' - }) - - expect(results[1]).toMatchObject({ - start: '2022-08-28T07:16:00.000Z', - stop: '2022-08-28T07:21:00.000Z', - title: 'Tölukubbar', - description: 'Lærið um tölustafina með Tölukubbunum! e.' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sjonvarp.is + +const { parser, url } = require('./sjonvarp.is.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'RUV', + xmltv_id: 'RUV.is' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=RUV&y=2022&m=08&d=28' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-28T07:15:00.000Z', + stop: '2022-08-28T07:16:00.000Z', + title: 'KrakkaRÚV' + }) + + expect(results[1]).toMatchObject({ + start: '2022-08-28T07:16:00.000Z', + stop: '2022-08-28T07:21:00.000Z', + title: 'Tölukubbar', + description: 'Lærið um tölustafina með Tölukubbunum! e.' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.co.nz/sky.co.nz.config.js b/sites/sky.co.nz/sky.co.nz.config.js index c095e5f3e..117fbd9ff 100644 --- a/sites/sky.co.nz/sky.co.nz.config.js +++ b/sites/sky.co.nz/sky.co.nz.config.js @@ -1,55 +1,55 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'sky.co.nz', - days: 2, - url({ date, channel }) { - return `https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=${ - channel.site_id - }&start=${date.valueOf()}&end=${date.add(1, 'day').valueOf()}&limit=20000` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.synopsis, - category: item.genres, - rating: parseRating(item), - start: dayjs(parseInt(item.start)), - stop: dayjs(parseInt(item.end)) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://skywebconfig.msl-prod.skycloud.co.nz/sky/json/channels.prod.json') - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'en', - site_id: parseInt(item.number).toString(), - name: item.name - } - }) - } -} - -function parseItems(content) { - const data = JSON.parse(content) - return data && data.events && Array.isArray(data.events) ? data.events : [] -} - -function parseRating(item) { - if (!item.rating) return null - return { - system: 'OFLC', - value: item.rating - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'sky.co.nz', + days: 2, + url({ date, channel }) { + return `https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=${ + channel.site_id + }&start=${date.valueOf()}&end=${date.add(1, 'day').valueOf()}&limit=20000` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.synopsis, + category: item.genres, + rating: parseRating(item), + start: dayjs(parseInt(item.start)), + stop: dayjs(parseInt(item.end)) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://skywebconfig.msl-prod.skycloud.co.nz/sky/json/channels.prod.json') + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'en', + site_id: parseInt(item.number).toString(), + name: item.name + } + }) + } +} + +function parseItems(content) { + const data = JSON.parse(content) + return data && data.events && Array.isArray(data.events) ? data.events : [] +} + +function parseRating(item) { + if (!item.rating) return null + return { + system: 'OFLC', + value: item.rating + } +} diff --git a/sites/sky.co.nz/sky.co.nz.test.js b/sites/sky.co.nz/sky.co.nz.test.js index ea6eb2b9b..291a2a612 100644 --- a/sites/sky.co.nz/sky.co.nz.test.js +++ b/sites/sky.co.nz/sky.co.nz.test.js @@ -1,62 +1,62 @@ -// npm run grab -- --site=sky.co.nz - -const { parser, url } = require('./sky.co.nz.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '36', - xmltv_id: 'SkyMoviesFamily.nz' -} -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=36&start=1674259200000&end=1674345600000&limit=20000' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result[0]).toMatchObject({ - title: 'Sing 2', - description: - "Animated: Buster Moon and his friends must persuade the world's most reclusive rock star to help launch their most dazzling extravaganza yet. Voices Of: Matthew McConaughey, Reese Witherspoon (2021)", - category: ['Animated'], - rating: { system: 'OFLC', value: 'PG' }, - start: '2023-01-20T23:41:00.000Z', - stop: '2023-01-21T01:28:00.000Z' - }) - - expect(result[5]).toMatchObject({ - title: 'Harry Potter and the Goblet of Fire', - description: - 'Adventure: Harry is selected to represent Hogwarts at a legendary and dangerous wizardry competition between three schools of magic. Stars: Daniel Radcliffe, Rupert Grint (2005)', - category: ['Action/Adventure'], - rating: { system: 'OFLC', value: 'M-V' }, - start: '2023-01-21T07:42:00.000Z', - stop: '2023-01-21T10:13:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const result = parser( - { - content: `{ - "code": "DATE_FORMAT_ERROR", - "description": "DateFormat error", - "message": "Unparseable date: x" - }` - }, - channel - ) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sky.co.nz + +const { parser, url } = require('./sky.co.nz.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '36', + xmltv_id: 'SkyMoviesFamily.nz' +} +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=36&start=1674259200000&end=1674345600000&limit=20000' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result[0]).toMatchObject({ + title: 'Sing 2', + description: + "Animated: Buster Moon and his friends must persuade the world's most reclusive rock star to help launch their most dazzling extravaganza yet. Voices Of: Matthew McConaughey, Reese Witherspoon (2021)", + category: ['Animated'], + rating: { system: 'OFLC', value: 'PG' }, + start: '2023-01-20T23:41:00.000Z', + stop: '2023-01-21T01:28:00.000Z' + }) + + expect(result[5]).toMatchObject({ + title: 'Harry Potter and the Goblet of Fire', + description: + 'Adventure: Harry is selected to represent Hogwarts at a legendary and dangerous wizardry competition between three schools of magic. Stars: Daniel Radcliffe, Rupert Grint (2005)', + category: ['Action/Adventure'], + rating: { system: 'OFLC', value: 'M-V' }, + start: '2023-01-21T07:42:00.000Z', + stop: '2023-01-21T10:13:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const result = parser( + { + content: `{ + "code": "DATE_FORMAT_ERROR", + "description": "DateFormat error", + "message": "Unparseable date: x" + }` + }, + channel + ) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.com/sky.com.config.js b/sites/sky.com/sky.com.config.js index cb9aa0847..72612bdf5 100644 --- a/sites/sky.com/sky.com.config.js +++ b/sites/sky.com/sky.com.config.js @@ -1,33 +1,33 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'sky.com', - days: 2, - url: function ({ date, channel }) { - return `https://epgservices.sky.com/5.2.2/api/2.0/channel/json/${ - channel.site_id - }/${date.unix()}/86400/4` - }, - parser: function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - - items.forEach(item => { - programs.push({ - title: item.t, - description: item.d, - start: dayjs.unix(item.s), - stop: dayjs.unix(item.s + item.m[1]), - icon: item.img ? `http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/${item.img}` : null - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - - return data && data.listings ? data.listings[channel.site_id] : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'sky.com', + days: 2, + url: function ({ date, channel }) { + return `https://epgservices.sky.com/5.2.2/api/2.0/channel/json/${ + channel.site_id + }/${date.unix()}/86400/4` + }, + parser: function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + + items.forEach(item => { + programs.push({ + title: item.t, + description: item.d, + start: dayjs.unix(item.s), + stop: dayjs.unix(item.s + item.m[1]), + icon: item.img ? `http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/${item.img}` : null + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + + return data && data.listings ? data.listings[channel.site_id] : [] +} diff --git a/sites/sky.com/sky.com.test.js b/sites/sky.com/sky.com.test.js index 91c45b189..5dc8872bb 100644 --- a/sites/sky.com/sky.com.test.js +++ b/sites/sky.com/sky.com.test.js @@ -1,54 +1,54 @@ -// npm run grab -- --site=sky.com - -const { parser, url } = require('./sky.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-12-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2002', - xmltv_id: 'BBCOneLondon.uk' -} -const content = - '{"listings":{"2002":[{"s":1639446600,"t":"Question of Sport","audioDescription":false,"m":[27170,1800,1,1,"--"],"rr":"S","subtitleHearing":true,"sid":53228,"d":"14/36. In this Christmas special, Paddy, Sam and Ugo are joined by Anton Du Beke, Shaun Wallace, Big Zuu and Jules Breach. Also in HD. [S]","img":"lisa/5.2.2/linear/channel/7f80ef03-3d8a-4f73-bf7d-6b03f410c7a8/2002"},{"s":1639448400,"t":"Weather for the Week Ahead","audioDescription":false,"m":[27171,300,1,1,"--"],"rr":"S","subtitleHearing":true,"sid":64799,"d":"Detailed weather forecast. Also in HD. [S]","img":"lisa/5.2.2/linear/channel/8fcf08b7-4081-499a-bf63-d100908e2d75/2002"}]}}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epgservices.sky.com/5.2.2/api/2.0/channel/json/2002/1639526400/86400/4' - ) -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-12-14T01:50:00.000Z', - stop: '2021-12-14T02:20:00.000Z', - title: 'Question of Sport', - icon: 'http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/lisa/5.2.2/linear/channel/7f80ef03-3d8a-4f73-bf7d-6b03f410c7a8/2002' - }, - { - start: '2021-12-14T02:20:00.000Z', - stop: '2021-12-14T02:25:00.000Z', - title: 'Weather for the Week Ahead', - icon: 'http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/lisa/5.2.2/linear/channel/8fcf08b7-4081-499a-bf63-d100908e2d75/2002' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"listings":{"2002":[]}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sky.com + +const { parser, url } = require('./sky.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-12-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2002', + xmltv_id: 'BBCOneLondon.uk' +} +const content = + '{"listings":{"2002":[{"s":1639446600,"t":"Question of Sport","audioDescription":false,"m":[27170,1800,1,1,"--"],"rr":"S","subtitleHearing":true,"sid":53228,"d":"14/36. In this Christmas special, Paddy, Sam and Ugo are joined by Anton Du Beke, Shaun Wallace, Big Zuu and Jules Breach. Also in HD. [S]","img":"lisa/5.2.2/linear/channel/7f80ef03-3d8a-4f73-bf7d-6b03f410c7a8/2002"},{"s":1639448400,"t":"Weather for the Week Ahead","audioDescription":false,"m":[27171,300,1,1,"--"],"rr":"S","subtitleHearing":true,"sid":64799,"d":"Detailed weather forecast. Also in HD. [S]","img":"lisa/5.2.2/linear/channel/8fcf08b7-4081-499a-bf63-d100908e2d75/2002"}]}}' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epgservices.sky.com/5.2.2/api/2.0/channel/json/2002/1639526400/86400/4' + ) +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-12-14T01:50:00.000Z', + stop: '2021-12-14T02:20:00.000Z', + title: 'Question of Sport', + icon: 'http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/lisa/5.2.2/linear/channel/7f80ef03-3d8a-4f73-bf7d-6b03f410c7a8/2002' + }, + { + start: '2021-12-14T02:20:00.000Z', + stop: '2021-12-14T02:25:00.000Z', + title: 'Weather for the Week Ahead', + icon: 'http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/lisa/5.2.2/linear/channel/8fcf08b7-4081-499a-bf63-d100908e2d75/2002' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"listings":{"2002":[]}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.de/sky.de.config.js b/sites/sky.de/sky.de.config.js index 26a14e0c3..fc1a98e83 100644 --- a/sites/sky.de/sky.de.config.js +++ b/sites/sky.de/sky.de.config.js @@ -1,50 +1,50 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'sky.de', - days: 2, - url: 'https://www.sky.de/sgtvg/service/getBroadcastsForGrid', - request: { - method: 'POST', - headers: { - 'accept-language': 'en-GB', - 'accept-encoding': 'gzip, deflate, br', - accept: 'application/json' - }, - data: function ({ channel, date }) { - return { - cil: [channel.site_id], - d: date.valueOf() - } - } - }, - parser: function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.et, - description: item.epit, - category: item.ec, - start: dayjs(item.bsdt), - stop: dayjs(item.bedt), - season: item.sn, - episode: item.en, - icon: item.pu ? `http://sky.de${item.pu}` : null - }) - }) - - return programs - } -} - -function parseContent(content, channel) { - const json = JSON.parse(content) - if (!Array.isArray(json.cl)) return null - return json.cl.find(i => i.ci == channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - return data && Array.isArray(data.el) ? data.el : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'sky.de', + days: 2, + url: 'https://www.sky.de/sgtvg/service/getBroadcastsForGrid', + request: { + method: 'POST', + headers: { + 'accept-language': 'en-GB', + 'accept-encoding': 'gzip, deflate, br', + accept: 'application/json' + }, + data: function ({ channel, date }) { + return { + cil: [channel.site_id], + d: date.valueOf() + } + } + }, + parser: function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.et, + description: item.epit, + category: item.ec, + start: dayjs(item.bsdt), + stop: dayjs(item.bedt), + season: item.sn, + episode: item.en, + icon: item.pu ? `http://sky.de${item.pu}` : null + }) + }) + + return programs + } +} + +function parseContent(content, channel) { + const json = JSON.parse(content) + if (!Array.isArray(json.cl)) return null + return json.cl.find(i => i.ci == channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + return data && Array.isArray(data.el) ? data.el : [] +} diff --git a/sites/sky.de/sky.de.test.js b/sites/sky.de/sky.de.test.js index 752179647..c91faabb2 100644 --- a/sites/sky.de/sky.de.test.js +++ b/sites/sky.de/sky.de.test.js @@ -1,68 +1,68 @@ -// npm run grab -- --site=sky.de - -const { parser, url, request } = require('./sky.de.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-02-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '522', - xmltv_id: 'WarnerTVComedyHD.de' -} - -const content = - '{"cl":[{"ci":522,"el":[{"ei":122309300,"bsdt":1645916700000,"bst":"00:05","bedt":1645918200000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Der Experte","sn":"4","en":"11","pu":"/static/img/program_guide/1522936_s.jpg"},{"ei":122309301,"bsdt":1645918200000,"bst":"00:30","bedt":1645919700000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Speedy Gonzales","sn":"4","en":"12","pu":"/static/img/program_guide/1522937_s.jpg"}]}]}' - -it('can generate valid url', () => { - expect(url).toBe('https://www.sky.de/sgtvg/service/getBroadcastsForGrid') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - cil: [channel.site_id], - d: date.valueOf() - }) -}) - -it('can parse response', () => { - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'King of Queens', - description: 'Der Experte', - category: 'Comedyserie', - start: '2022-02-26T23:05:00.000Z', - stop: '2022-02-26T23:30:00.000Z', - season: '4', - episode: '11', - icon: 'http://sky.de/static/img/program_guide/1522936_s.jpg' - }, - { - title: 'King of Queens', - description: 'Speedy Gonzales', - category: 'Comedyserie', - start: '2022-02-26T23:30:00.000Z', - stop: '2022-02-26T23:55:00.000Z', - season: '4', - episode: '12', - icon: 'http://sky.de/static/img/program_guide/1522937_s.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sky.de + +const { parser, url, request } = require('./sky.de.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-02-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '522', + xmltv_id: 'WarnerTVComedyHD.de' +} + +const content = + '{"cl":[{"ci":522,"el":[{"ei":122309300,"bsdt":1645916700000,"bst":"00:05","bedt":1645918200000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Der Experte","sn":"4","en":"11","pu":"/static/img/program_guide/1522936_s.jpg"},{"ei":122309301,"bsdt":1645918200000,"bst":"00:30","bedt":1645919700000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Speedy Gonzales","sn":"4","en":"12","pu":"/static/img/program_guide/1522937_s.jpg"}]}]}' + +it('can generate valid url', () => { + expect(url).toBe('https://www.sky.de/sgtvg/service/getBroadcastsForGrid') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + cil: [channel.site_id], + d: date.valueOf() + }) +}) + +it('can parse response', () => { + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'King of Queens', + description: 'Der Experte', + category: 'Comedyserie', + start: '2022-02-26T23:05:00.000Z', + stop: '2022-02-26T23:30:00.000Z', + season: '4', + episode: '11', + icon: 'http://sky.de/static/img/program_guide/1522936_s.jpg' + }, + { + title: 'King of Queens', + description: 'Speedy Gonzales', + category: 'Comedyserie', + start: '2022-02-26T23:30:00.000Z', + stop: '2022-02-26T23:55:00.000Z', + season: '4', + episode: '12', + icon: 'http://sky.de/static/img/program_guide/1522937_s.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sportsnet.ca/sportsnet.ca.config.js b/sites/sportsnet.ca/sportsnet.ca.config.js index 247902b13..deed0683c 100644 --- a/sites/sportsnet.ca/sportsnet.ca.config.js +++ b/sites/sportsnet.ca/sportsnet.ca.config.js @@ -1,50 +1,50 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'sportsnet.ca', - days: 2, - url: function ({ channel, date }) { - return `https://production-cdn.sportsnet.ca/api/schedules?channels=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}&duration=24&hour=0` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.item.title, - description: item.item.shortDescription, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseIcon(item) { - if (!item.item || !item.item.images) return null - - return item.item.images.tile -} - -function parseStart(item) { - return dayjs.utc(item.startDate) -} - -function parseStop(item) { - return dayjs.utc(item.endDate) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!Array.isArray(data) || !Array.isArray(data[0].schedules)) return [] - - return data[0].schedules -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'sportsnet.ca', + days: 2, + url: function ({ channel, date }) { + return `https://production-cdn.sportsnet.ca/api/schedules?channels=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}&duration=24&hour=0` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.item.title, + description: item.item.shortDescription, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseIcon(item) { + if (!item.item || !item.item.images) return null + + return item.item.images.tile +} + +function parseStart(item) { + return dayjs.utc(item.startDate) +} + +function parseStop(item) { + return dayjs.utc(item.endDate) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!Array.isArray(data) || !Array.isArray(data[0].schedules)) return [] + + return data[0].schedules +} diff --git a/sites/sportsnet.ca/sportsnet.ca.test.js b/sites/sportsnet.ca/sportsnet.ca.test.js index 9b00e5e86..49efa7d95 100644 --- a/sites/sportsnet.ca/sportsnet.ca.test.js +++ b/sites/sportsnet.ca/sportsnet.ca.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=sportsnet.ca - -const { parser, url } = require('./sportsnet.ca.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '24533', - xmltv_id: 'SportsNetOntario.ca' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://production-cdn.sportsnet.ca/api/schedules?channels=24533&date=2022-03-14&duration=24&hour=0' - ) -}) - -it('can parse response', () => { - const content = - '[{"channelId":"24533","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[{"channelId":"24533","customFields":{"ContentId":"EP029977175139","Checksum":"2DA90E7E66B9C311F98B186B89C50FAD"},"endDate":"2022-03-14T02:30:00Z","id":"826cb731-9de4-4cf3-bcca-d548d8a33d16","startDate":"2022-03-14T00:00:00Z","item":{"id":"34a028b0-eacf-40f3-9bf9-62ee3330df1b","type":"program","title":"Calgary Flames at Colorado Avalanche","shortDescription":"Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.","path":"/channel/24533","duration":9000,"images":{"tile":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785305\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160","wallpaper":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785311\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160"}}}]}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-14T00:00:00.000Z', - stop: '2022-03-14T02:30:00.000Z', - title: 'Calgary Flames at Colorado Avalanche', - description: - 'Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.', - icon: "https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='785305'&EntityType='LinearSchedule'&EntityId='826cb731-9de4-4cf3-bcca-d548d8a33d16'&Width=3840&Height=2160" - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '[{"channelId":"245321","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[]}]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sportsnet.ca + +const { parser, url } = require('./sportsnet.ca.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '24533', + xmltv_id: 'SportsNetOntario.ca' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://production-cdn.sportsnet.ca/api/schedules?channels=24533&date=2022-03-14&duration=24&hour=0' + ) +}) + +it('can parse response', () => { + const content = + '[{"channelId":"24533","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[{"channelId":"24533","customFields":{"ContentId":"EP029977175139","Checksum":"2DA90E7E66B9C311F98B186B89C50FAD"},"endDate":"2022-03-14T02:30:00Z","id":"826cb731-9de4-4cf3-bcca-d548d8a33d16","startDate":"2022-03-14T00:00:00Z","item":{"id":"34a028b0-eacf-40f3-9bf9-62ee3330df1b","type":"program","title":"Calgary Flames at Colorado Avalanche","shortDescription":"Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.","path":"/channel/24533","duration":9000,"images":{"tile":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785305\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160","wallpaper":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785311\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160"}}}]}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-14T00:00:00.000Z', + stop: '2022-03-14T02:30:00.000Z', + title: 'Calgary Flames at Colorado Avalanche', + description: + 'Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.', + icon: "https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='785305'&EntityType='LinearSchedule'&EntityId='826cb731-9de4-4cf3-bcca-d548d8a33d16'&Width=3840&Height=2160" + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '[{"channelId":"245321","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[]}]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/starhubtvplus.com/starhubtvplus.com.config.js b/sites/starhubtvplus.com/starhubtvplus.com.config.js index 66a78cea0..3fe6553ac 100644 --- a/sites/starhubtvplus.com/starhubtvplus.com.config.js +++ b/sites/starhubtvplus.com/starhubtvplus.com.config.js @@ -1,85 +1,85 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const APP_KEY = '5ee2ef931de1c4001b2e7fa3_5ee2ec25a0e845001c1783dc' -const SESSION_KEY = '01G2QG0N3RWDNCBA1S5MK1MD2K17CE4431A2' - -module.exports = { - site: 'starhubtvplus.com', - days: 2, - request: { - headers: { - 'x-application-key': APP_KEY, - 'x-application-session': SESSION_KEY - }, - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ date }) { - const variables = JSON.stringify({ - category: '', - dateFrom: date.format('YYYY-MM-DD'), - dateTo: date.add(1, 'd').format('YYYY-MM-DD') - }) - const query = - 'query webFilteredEpg($category: String, $dateFrom: DateWithoutTime, $dateTo: DateWithoutTime!) { nagraEpg(category: $category) { items { id: tvChannel image name: longName programs: programsByDate(dateFrom: $dateFrom, dateTo: $dateTo) { id title description Categories startTime endTime }}}}' - - const params = `operationName=webFilteredEpg&variables=${encodeURIComponent( - variables - )}&query=${encodeURIComponent(query)}` - - return `https://api.starhubtvplus.com/epg?${params}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - category: item.Categories, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get( - 'https://api.starhubtvplus.com/epg?operationName=webFilteredEpg&variables=%7B%22category%22%3A%22%22,%22dateFrom%22%3A%222022-05-10%22,%22dateTo%22%3A%222022-05-11%22%7D&query=query%20webFilteredEpg(%24category%3A%20String)%20%7B%20nagraEpg(category%3A%20%24category)%20%7B%20items%20%7B%20id%3A%20tvChannel%20image%20name%3A%20longName%20%7D%7D%7D', - { - headers: { - 'x-application-key': APP_KEY, - 'x-application-session': SESSION_KEY - } - } - ) - .then(r => r.data.data.nagraEpg.items) - .catch(console.log) - - return items.map(item => ({ - site_id: item.id, - name: item.name.replace('_DASH', '') - })) - } -} - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !data.data || !data.data.nagraEpg || !Array.isArray(data.data.nagraEpg.items)) - return [] - const ch = data.data.nagraEpg.items.find(ch => ch.id == channel.site_id) - - return ch && Array.isArray(ch.programs) ? ch.programs : [] -} +const axios = require('axios') +const dayjs = require('dayjs') + +const APP_KEY = '5ee2ef931de1c4001b2e7fa3_5ee2ec25a0e845001c1783dc' +const SESSION_KEY = '01G2QG0N3RWDNCBA1S5MK1MD2K17CE4431A2' + +module.exports = { + site: 'starhubtvplus.com', + days: 2, + request: { + headers: { + 'x-application-key': APP_KEY, + 'x-application-session': SESSION_KEY + }, + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ date }) { + const variables = JSON.stringify({ + category: '', + dateFrom: date.format('YYYY-MM-DD'), + dateTo: date.add(1, 'd').format('YYYY-MM-DD') + }) + const query = + 'query webFilteredEpg($category: String, $dateFrom: DateWithoutTime, $dateTo: DateWithoutTime!) { nagraEpg(category: $category) { items { id: tvChannel image name: longName programs: programsByDate(dateFrom: $dateFrom, dateTo: $dateTo) { id title description Categories startTime endTime }}}}' + + const params = `operationName=webFilteredEpg&variables=${encodeURIComponent( + variables + )}&query=${encodeURIComponent(query)}` + + return `https://api.starhubtvplus.com/epg?${params}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + category: item.Categories, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get( + 'https://api.starhubtvplus.com/epg?operationName=webFilteredEpg&variables=%7B%22category%22%3A%22%22,%22dateFrom%22%3A%222022-05-10%22,%22dateTo%22%3A%222022-05-11%22%7D&query=query%20webFilteredEpg(%24category%3A%20String)%20%7B%20nagraEpg(category%3A%20%24category)%20%7B%20items%20%7B%20id%3A%20tvChannel%20image%20name%3A%20longName%20%7D%7D%7D', + { + headers: { + 'x-application-key': APP_KEY, + 'x-application-session': SESSION_KEY + } + } + ) + .then(r => r.data.data.nagraEpg.items) + .catch(console.log) + + return items.map(item => ({ + site_id: item.id, + name: item.name.replace('_DASH', '') + })) + } +} + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !data.data || !data.data.nagraEpg || !Array.isArray(data.data.nagraEpg.items)) + return [] + const ch = data.data.nagraEpg.items.find(ch => ch.id == channel.site_id) + + return ch && Array.isArray(ch.programs) ? ch.programs : [] +} diff --git a/sites/starhubtvplus.com/starhubtvplus.com.test.js b/sites/starhubtvplus.com/starhubtvplus.com.test.js index ac37133ba..7819a9aa8 100644 --- a/sites/starhubtvplus.com/starhubtvplus.com.test.js +++ b/sites/starhubtvplus.com/starhubtvplus.com.test.js @@ -1,63 +1,63 @@ -// npm run channels:parse -- --config=sites/starhubtvplus.com/starhubtvplus.com.config.js --output=sites/starhubtvplus.com/starhubtvplus.com.channels.xml -// npm run grab -- --site=starhubtvplus.com - -const { parser, url, request } = require('./starhubtvplus.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '102', - xmltv_id: 'Channel5.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.starhubtvplus.com/epg?operationName=webFilteredEpg&variables=%7B%22category%22%3A%22%22%2C%22dateFrom%22%3A%222022-05-10%22%2C%22dateTo%22%3A%222022-05-11%22%7D&query=query%20webFilteredEpg(%24category%3A%20String%2C%20%24dateFrom%3A%20DateWithoutTime%2C%20%24dateTo%3A%20DateWithoutTime!)%20%7B%20nagraEpg(category%3A%20%24category)%20%7B%20items%20%7B%20id%3A%20tvChannel%20image%20name%3A%20longName%20programs%3A%20programsByDate(dateFrom%3A%20%24dateFrom%2C%20dateTo%3A%20%24dateTo)%20%7B%20id%20title%20description%20Categories%20startTime%20endTime%20%7D%7D%7D%7D' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-application-key': '5ee2ef931de1c4001b2e7fa3_5ee2ec25a0e845001c1783dc', - 'x-application-session': '01G2QG0N3RWDNCBA1S5MK1MD2K17CE4431A2' - }) -}) - -it('can generate valid cache settings', () => { - expect(request.cache).toMatchObject({ - ttl: 60 * 60 * 1000 - }) -}) - -it('can parse response', () => { - const content = - '{"data":{"nagraEpg":{"items":[{"id":102,"name":"Channel 5 HD_DASH","programs":[{"id":"GLOBAL_TC0021650123","title":"Luke Nguyen\'s Vietnam","description":"Luke leaves the hustle and bustle of Hanoi behind for the mystical mountains of Sapa. There, he prepares some black chicken in and amongst the local streets. He cooks buffalo for a salad in the busy Sapa markets, as well as a tofu-and-tomato dish high up in the rice paddy fields with the most spectacular backdrop.","Categories":["Others"],"startTime":1652110200000,"endTime":1652112000000}]}]}}}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-05-09T15:30:00.000Z', - stop: '2022-05-09T16:00:00.000Z', - title: "Luke Nguyen's Vietnam", - description: - 'Luke leaves the hustle and bustle of Hanoi behind for the mystical mountains of Sapa. There, he prepares some black chicken in and amongst the local streets. He cooks buffalo for a salad in the busy Sapa markets, as well as a tofu-and-tomato dish high up in the rice paddy fields with the most spectacular backdrop.', - category: ['Others'] - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '{"errors":[{"code":"A9999","message":"Syntax, request headers or server error","extendedLogging":{"message":"Cannot read property \'operation\' of undefined"}}]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/starhubtvplus.com/starhubtvplus.com.config.js --output=sites/starhubtvplus.com/starhubtvplus.com.channels.xml +// npm run grab -- --site=starhubtvplus.com + +const { parser, url, request } = require('./starhubtvplus.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '102', + xmltv_id: 'Channel5.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.starhubtvplus.com/epg?operationName=webFilteredEpg&variables=%7B%22category%22%3A%22%22%2C%22dateFrom%22%3A%222022-05-10%22%2C%22dateTo%22%3A%222022-05-11%22%7D&query=query%20webFilteredEpg(%24category%3A%20String%2C%20%24dateFrom%3A%20DateWithoutTime%2C%20%24dateTo%3A%20DateWithoutTime!)%20%7B%20nagraEpg(category%3A%20%24category)%20%7B%20items%20%7B%20id%3A%20tvChannel%20image%20name%3A%20longName%20programs%3A%20programsByDate(dateFrom%3A%20%24dateFrom%2C%20dateTo%3A%20%24dateTo)%20%7B%20id%20title%20description%20Categories%20startTime%20endTime%20%7D%7D%7D%7D' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-application-key': '5ee2ef931de1c4001b2e7fa3_5ee2ec25a0e845001c1783dc', + 'x-application-session': '01G2QG0N3RWDNCBA1S5MK1MD2K17CE4431A2' + }) +}) + +it('can generate valid cache settings', () => { + expect(request.cache).toMatchObject({ + ttl: 60 * 60 * 1000 + }) +}) + +it('can parse response', () => { + const content = + '{"data":{"nagraEpg":{"items":[{"id":102,"name":"Channel 5 HD_DASH","programs":[{"id":"GLOBAL_TC0021650123","title":"Luke Nguyen\'s Vietnam","description":"Luke leaves the hustle and bustle of Hanoi behind for the mystical mountains of Sapa. There, he prepares some black chicken in and amongst the local streets. He cooks buffalo for a salad in the busy Sapa markets, as well as a tofu-and-tomato dish high up in the rice paddy fields with the most spectacular backdrop.","Categories":["Others"],"startTime":1652110200000,"endTime":1652112000000}]}]}}}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-05-09T15:30:00.000Z', + stop: '2022-05-09T16:00:00.000Z', + title: "Luke Nguyen's Vietnam", + description: + 'Luke leaves the hustle and bustle of Hanoi behind for the mystical mountains of Sapa. There, he prepares some black chicken in and amongst the local streets. He cooks buffalo for a salad in the busy Sapa markets, as well as a tofu-and-tomato dish high up in the rice paddy fields with the most spectacular backdrop.', + category: ['Others'] + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '{"errors":[{"code":"A9999","message":"Syntax, request headers or server error","extendedLogging":{"message":"Cannot read property \'operation\' of undefined"}}]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/startimestv.com/startimestv.com.config.js b/sites/startimestv.com/startimestv.com.config.js index 42f255f95..8944afc87 100644 --- a/sites/startimestv.com/startimestv.com.config.js +++ b/sites/startimestv.com/startimestv.com.config.js @@ -1,115 +1,115 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'startimestv.com', - days: 2, - url: function ({ channel, date }) { - return `https://www.startimestv.com/channeldetail/${channel.site_id}/${date.format( - 'YYYY-MM-DD' - )}.html` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - programs.push({ - title: parseTitle($item), - season: parseSeason($item), - episode: parseEpisode($item), - description: parseDescription($item), - start: parseStart($item, date), - stop: parseStop($item, date) - }) - }) - - return programs - }, - async channels({ country }) { - const area = { - ke: 6, - ng: 2, - tz: 3, - ug: 4, - rw: 5, - gh: 32, - mw: 14, - ci: 22, - gn: 12, - bi: 9, - cg: 16, - cd: 11, - mg: 13, - mz: 15, - cm: 20, - ga: 19 - } - const data = await axios - .get('https://www.startimestv.com/tv_guide.html', { - headers: { - Cookie: `default_areaID=${area[country]}` - } - }) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - const script = $('body > script:nth-child(10)').html() - let [, json] = script.match(/var obj = eval\( '(.*)' \);/) || [null, null] - json = json.replace(/\\'/g, '') - const items = JSON.parse(json) - - return items.map(i => ({ - name: i.name, - site_id: i.id - })) - } -} - -function parseStart($item, date) { - const time = $item('.in > .t').text() - const [, HH, mm] = time.match(/^(\d{2}):(\d{2})/) || [null, null, null] - - return HH && mm ? dayjs.utc(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm') : null -} - -function parseStop($item, date) { - const time = $item('.in > .t').text() - const [, HH, mm] = time.match(/(\d{2}):(\d{2})$/) || [null, null, null] - - return HH && mm ? dayjs.utc(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm') : null -} - -function parseSeason($item) { - const title = parseTitle($item) - const [, season] = title.match(/ S(\d+)/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - const title = parseTitle($item) - const [, episode] = title.match(/ E(\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseTitle($item) { - return $item('.in > h3').text() -} - -function parseDescription($item) { - return $item('.in > p').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('div.tv_gui > div.list > div > div').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'startimestv.com', + days: 2, + url: function ({ channel, date }) { + return `https://www.startimestv.com/channeldetail/${channel.site_id}/${date.format( + 'YYYY-MM-DD' + )}.html` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + programs.push({ + title: parseTitle($item), + season: parseSeason($item), + episode: parseEpisode($item), + description: parseDescription($item), + start: parseStart($item, date), + stop: parseStop($item, date) + }) + }) + + return programs + }, + async channels({ country }) { + const area = { + ke: 6, + ng: 2, + tz: 3, + ug: 4, + rw: 5, + gh: 32, + mw: 14, + ci: 22, + gn: 12, + bi: 9, + cg: 16, + cd: 11, + mg: 13, + mz: 15, + cm: 20, + ga: 19 + } + const data = await axios + .get('https://www.startimestv.com/tv_guide.html', { + headers: { + Cookie: `default_areaID=${area[country]}` + } + }) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + const script = $('body > script:nth-child(10)').html() + let [, json] = script.match(/var obj = eval\( '(.*)' \);/) || [null, null] + json = json.replace(/\\'/g, '') + const items = JSON.parse(json) + + return items.map(i => ({ + name: i.name, + site_id: i.id + })) + } +} + +function parseStart($item, date) { + const time = $item('.in > .t').text() + const [, HH, mm] = time.match(/^(\d{2}):(\d{2})/) || [null, null, null] + + return HH && mm ? dayjs.utc(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm') : null +} + +function parseStop($item, date) { + const time = $item('.in > .t').text() + const [, HH, mm] = time.match(/(\d{2}):(\d{2})$/) || [null, null, null] + + return HH && mm ? dayjs.utc(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm') : null +} + +function parseSeason($item) { + const title = parseTitle($item) + const [, season] = title.match(/ S(\d+)/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + const title = parseTitle($item) + const [, episode] = title.match(/ E(\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseTitle($item) { + return $item('.in > h3').text() +} + +function parseDescription($item) { + return $item('.in > p').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('div.tv_gui > div.list > div > div').toArray() +} diff --git a/sites/startimestv.com/startimestv.com.test.js b/sites/startimestv.com/startimestv.com.test.js index 2ac19623c..a11aecda7 100644 --- a/sites/startimestv.com/startimestv.com.test.js +++ b/sites/startimestv.com/startimestv.com.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/startimestv.com/startimestv.com.config.js --output=./sites/startimestv.com/startimestv.com.channels.xml --set=country:ke -// npm run grab -- --site=startimestv.com - -const { parser, url } = require('./startimestv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-04-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1023102509', - xmltv_id: 'ZeeOneAfrica.za' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.startimestv.com/channeldetail/1023102509/2022-04-10.html' - ) -}) - -it('can parse response', () => { - const content = - '

    Guddan S2 E77

    00:00-01:00

    Vickrant is overjoyed to see Akshat in pain and not knowing what to do.

    00:00-01:00 Guddan S2 E77

    Vickrant is overjoyed to see Akshat in pain and not knowing what to do.

    ' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-04-10T00:00:00.000Z', - stop: '2022-04-10T01:00:00.000Z', - title: 'Guddan S2 E77', - season: 2, - episode: 77, - description: 'Vickrant is overjoyed to see Akshat in pain and not knowing what to do.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '

    Rate:

    Category:


    ' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/startimestv.com/startimestv.com.config.js --output=./sites/startimestv.com/startimestv.com.channels.xml --set=country:ke +// npm run grab -- --site=startimestv.com + +const { parser, url } = require('./startimestv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-04-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1023102509', + xmltv_id: 'ZeeOneAfrica.za' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.startimestv.com/channeldetail/1023102509/2022-04-10.html' + ) +}) + +it('can parse response', () => { + const content = + '

    Guddan S2 E77

    00:00-01:00

    Vickrant is overjoyed to see Akshat in pain and not knowing what to do.

    00:00-01:00 Guddan S2 E77

    Vickrant is overjoyed to see Akshat in pain and not knowing what to do.

    ' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-04-10T00:00:00.000Z', + stop: '2022-04-10T01:00:00.000Z', + title: 'Guddan S2 E77', + season: 2, + episode: 77, + description: 'Vickrant is overjoyed to see Akshat in pain and not knowing what to do.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '

    Rate:

    Category:


    ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/startv.com/startv.com.config.js b/sites/startv.com/startv.com.config.js index 25fa1fbb2..a31b0c1c6 100644 --- a/sites/startv.com/startv.com.config.js +++ b/sites/startv.com/startv.com.config.js @@ -1,88 +1,88 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.startv.com/umbraco/api/startvguideproxy' - -module.exports = { - site: 'startv.com', - days: 2, - url: `${API_ENDPOINT}/GetTvGuideSchedule`, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8' - }, - data({ channel, date }) { - return { - Channels: channel.site_id, - Start: date.format('YYYYMMDDHHmm'), - Stop: date.add(1, 'd').format('YYYYMMDDHHmm') - } - } - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.desc, - icon: item.programmeurl, - category: item.subgenre, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .post( - `${API_ENDPOINT}/GetChannelResult`, - { Genre: 'All Channels' }, - { - headers: { - 'Content-Type': 'application/json; charset=UTF-8' - } - } - ) - .then(r => JSON.parse(r.data)) - .catch(console.log) - - const channels = data.channelsbygenreandlanguage.channellist.channelnames.split(',') - return channels.map(item => { - return { - lang: 'hi', - site_id: item, - name: item - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.start, 'YYYYMMDDHHmm', 'Asia/Kolkata') -} - -function parseStop(item) { - return dayjs.tz(item.stop, 'YYYYMMDDHHmm', 'Asia/Kolkata') -} - -function parseItems(content, channel) { - if (!content.length) return [] - const json = JSON.parse(content) - if (!json.length) return [] - const data = JSON.parse(json) - if (!data || !data.ScheduleGrid || !Array.isArray(data.ScheduleGrid.channel)) return [] - const channelData = data.ScheduleGrid.channel.find(c => c.channeldisplayname === channel.site_id) - - return channelData && Array.isArray(channelData.programme) ? channelData.programme : [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.startv.com/umbraco/api/startvguideproxy' + +module.exports = { + site: 'startv.com', + days: 2, + url: `${API_ENDPOINT}/GetTvGuideSchedule`, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8' + }, + data({ channel, date }) { + return { + Channels: channel.site_id, + Start: date.format('YYYYMMDDHHmm'), + Stop: date.add(1, 'd').format('YYYYMMDDHHmm') + } + } + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.desc, + icon: item.programmeurl, + category: item.subgenre, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .post( + `${API_ENDPOINT}/GetChannelResult`, + { Genre: 'All Channels' }, + { + headers: { + 'Content-Type': 'application/json; charset=UTF-8' + } + } + ) + .then(r => JSON.parse(r.data)) + .catch(console.log) + + const channels = data.channelsbygenreandlanguage.channellist.channelnames.split(',') + return channels.map(item => { + return { + lang: 'hi', + site_id: item, + name: item + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.start, 'YYYYMMDDHHmm', 'Asia/Kolkata') +} + +function parseStop(item) { + return dayjs.tz(item.stop, 'YYYYMMDDHHmm', 'Asia/Kolkata') +} + +function parseItems(content, channel) { + if (!content.length) return [] + const json = JSON.parse(content) + if (!json.length) return [] + const data = JSON.parse(json) + if (!data || !data.ScheduleGrid || !Array.isArray(data.ScheduleGrid.channel)) return [] + const channelData = data.ScheduleGrid.channel.find(c => c.channeldisplayname === channel.site_id) + + return channelData && Array.isArray(channelData.programme) ? channelData.programme : [] +} diff --git a/sites/startv.com/startv.com.test.js b/sites/startv.com/startv.com.test.js index 7482f8c79..e39b5fa3b 100644 --- a/sites/startv.com/startv.com.test.js +++ b/sites/startv.com/startv.com.test.js @@ -1,66 +1,66 @@ -// npm run channels:parse -- --config=sites/startv.com/startv.com.config.js --output=sites/startv.com/startv.com.channels.xml -// npm run grab -- --site=startv.com - -const { parser, url, request } = require('./startv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-31', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'STAR PLUS', - xmltv_id: 'StarPlus.in' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.startv.com/umbraco/api/startvguideproxy/GetTvGuideSchedule') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - Channels: 'STAR PLUS', - Start: '202203310000', - Stop: '202204010000' - }) -}) - -it('can parse response', () => { - const content = - '"{\\"ScheduleGrid\\":{\\"channel\\":[{\\"id\\":null,\\"displayname\\":null,\\"channelid\\":\\"10000000000080000\\",\\"channellogourl\\":\\"http://imagesstartv.whatsonindia.com/dasimages/channel/landscape/100x75/wHtcYVRZ.png\\",\\"channelgenre\\":\\"Hindi Entertainment\\",\\"channelweburl\\":\\"\\",\\"channeldisplayname\\":\\"STAR PLUS\\",\\"lcn\\":\\"1\\",\\"isfav\\":\\"0\\",\\"programme\\":[{\\"programmeid\\":\\"30000000550792674\\",\\"title\\":\\"Imlie\\",\\"start\\":\\"202203310000\\",\\"stop\\":\\"202203310030\\",\\"desc\\":\\"Imlie finds herself in deep trouble when she gets tied up before the wedding. Meanwhile, Aryan assumes that he is getting married to Imlie and performs the wedding rituals.\\",\\"programmeurl\\":\\"http://imagesstartv.whatsonindia.com/dasimages/landscape/360x270/59A9215E5DE13ABF4B05C59A6C87768AD61CA608M.jpg\\",\\"channelid\\":\\"10000000000080000\\",\\"date\\":\\"20220331\\",\\"episodenum\\":null,\\"subtitle\\":null,\\"scheduleid\\":\\"10000069158583187\\",\\"genre\\":\\"TV Show\\",\\"subgenre\\":\\"Drama\\",\\"programmescore\\":\\"0.083309\\",\\"languagename\\":\\"Hindi\\",\\"dubbedlanguageid\\":\\"10000000000040000\\",\\"timestring\\":\\"12:00 AM, Tomorrow\\",\\"duration\\":\\"30\\",\\"episodeshorttitle\\":\\"\\"}]}]}}"' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-30T18:30:00.000Z', - stop: '2022-03-30T19:00:00.000Z', - title: 'Imlie', - description: - 'Imlie finds herself in deep trouble when she gets tied up before the wedding. Meanwhile, Aryan assumes that he is getting married to Imlie and performs the wedding rituals.', - icon: 'http://imagesstartv.whatsonindia.com/dasimages/landscape/360x270/59A9215E5DE13ABF4B05C59A6C87768AD61CA608M.jpg', - category: 'Drama' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '""' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/startv.com/startv.com.config.js --output=sites/startv.com/startv.com.channels.xml +// npm run grab -- --site=startv.com + +const { parser, url, request } = require('./startv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-31', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'STAR PLUS', + xmltv_id: 'StarPlus.in' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.startv.com/umbraco/api/startvguideproxy/GetTvGuideSchedule') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + Channels: 'STAR PLUS', + Start: '202203310000', + Stop: '202204010000' + }) +}) + +it('can parse response', () => { + const content = + '"{\\"ScheduleGrid\\":{\\"channel\\":[{\\"id\\":null,\\"displayname\\":null,\\"channelid\\":\\"10000000000080000\\",\\"channellogourl\\":\\"http://imagesstartv.whatsonindia.com/dasimages/channel/landscape/100x75/wHtcYVRZ.png\\",\\"channelgenre\\":\\"Hindi Entertainment\\",\\"channelweburl\\":\\"\\",\\"channeldisplayname\\":\\"STAR PLUS\\",\\"lcn\\":\\"1\\",\\"isfav\\":\\"0\\",\\"programme\\":[{\\"programmeid\\":\\"30000000550792674\\",\\"title\\":\\"Imlie\\",\\"start\\":\\"202203310000\\",\\"stop\\":\\"202203310030\\",\\"desc\\":\\"Imlie finds herself in deep trouble when she gets tied up before the wedding. Meanwhile, Aryan assumes that he is getting married to Imlie and performs the wedding rituals.\\",\\"programmeurl\\":\\"http://imagesstartv.whatsonindia.com/dasimages/landscape/360x270/59A9215E5DE13ABF4B05C59A6C87768AD61CA608M.jpg\\",\\"channelid\\":\\"10000000000080000\\",\\"date\\":\\"20220331\\",\\"episodenum\\":null,\\"subtitle\\":null,\\"scheduleid\\":\\"10000069158583187\\",\\"genre\\":\\"TV Show\\",\\"subgenre\\":\\"Drama\\",\\"programmescore\\":\\"0.083309\\",\\"languagename\\":\\"Hindi\\",\\"dubbedlanguageid\\":\\"10000000000040000\\",\\"timestring\\":\\"12:00 AM, Tomorrow\\",\\"duration\\":\\"30\\",\\"episodeshorttitle\\":\\"\\"}]}]}}"' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-30T18:30:00.000Z', + stop: '2022-03-30T19:00:00.000Z', + title: 'Imlie', + description: + 'Imlie finds herself in deep trouble when she gets tied up before the wedding. Meanwhile, Aryan assumes that he is getting married to Imlie and performs the wedding rituals.', + icon: 'http://imagesstartv.whatsonindia.com/dasimages/landscape/360x270/59A9215E5DE13ABF4B05C59A6C87768AD61CA608M.jpg', + category: 'Drama' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '""' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/streamingtvguides.com/streamingtvguides.com.config.js b/sites/streamingtvguides.com/streamingtvguides.com.config.js index 4a14fdb17..6a6768d0f 100644 --- a/sites/streamingtvguides.com/streamingtvguides.com.config.js +++ b/sites/streamingtvguides.com/streamingtvguides.com.config.js @@ -1,70 +1,70 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') -const _ = require('lodash') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -module.exports = { - site: 'streamingtvguides.com', - days: 2, - url({ channel }) { - return `https://streamingtvguides.com/Channel/${channel.site_id}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item) - if (!date.isSame(start, 'd')) return - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop: parseStop($item) - }) - }) - - programs = _.orderBy(_.uniqBy(programs, 'start'), 'start') - - return programs - } -} - -function parseTitle($item) { - return $item('.card-body > .prog-contains > .card-title') - .clone() - .children() - .remove() - .end() - .text() - .trim() -} - -function parseDescription($item) { - return $item('.card-body > .card-text').clone().children().remove().end().text().trim() -} - -function parseStart($item) { - const date = $item('.card-body').clone().children().remove().end().text().trim() - const [time] = date.split(' - ') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() -} - -function parseStop($item) { - const date = $item('.card-body').clone().children().remove().end().text().trim() - const [, time] = date.split(' - ') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.container').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') +const _ = require('lodash') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +module.exports = { + site: 'streamingtvguides.com', + days: 2, + url({ channel }) { + return `https://streamingtvguides.com/Channel/${channel.site_id}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item) + if (!date.isSame(start, 'd')) return + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop: parseStop($item) + }) + }) + + programs = _.orderBy(_.uniqBy(programs, 'start'), 'start') + + return programs + } +} + +function parseTitle($item) { + return $item('.card-body > .prog-contains > .card-title') + .clone() + .children() + .remove() + .end() + .text() + .trim() +} + +function parseDescription($item) { + return $item('.card-body > .card-text').clone().children().remove().end().text().trim() +} + +function parseStart($item) { + const date = $item('.card-body').clone().children().remove().end().text().trim() + const [time] = date.split(' - ') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() +} + +function parseStop($item) { + const date = $item('.card-body').clone().children().remove().end().text().trim() + const [, time] = date.split(' - ') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.container').toArray() +} diff --git a/sites/streamingtvguides.com/streamingtvguides.com.test.js b/sites/streamingtvguides.com/streamingtvguides.com.test.js index 1d6400962..fcf8fbc34 100644 --- a/sites/streamingtvguides.com/streamingtvguides.com.test.js +++ b/sites/streamingtvguides.com/streamingtvguides.com.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=streamingtvguides.com - -const { parser, url } = require('./streamingtvguides.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'GMAPNY', - xmltv_id: 'GMAPinoyTVUSACanada.ph' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://streamingtvguides.com/Channel/GMAPNY') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(38) - - expect(results[0]).toMatchObject({ - start: '2023-06-27T00:40:00.000Z', - stop: '2023-06-27T02:00:00.000Z', - title: '24 Oras', - description: 'Up to the minute news around the world.' - }) - - expect(results[37]).toMatchObject({ - start: '2023-06-27T21:50:00.000Z', - stop: '2023-06-28T00:00:00.000Z', - title: 'Eat Bulaga', - description: 'Rousing and engrossing segments with engaging hosts.' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - const result = parser({ - date, - content - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=streamingtvguides.com + +const { parser, url } = require('./streamingtvguides.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'GMAPNY', + xmltv_id: 'GMAPinoyTVUSACanada.ph' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://streamingtvguides.com/Channel/GMAPNY') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(38) + + expect(results[0]).toMatchObject({ + start: '2023-06-27T00:40:00.000Z', + stop: '2023-06-27T02:00:00.000Z', + title: '24 Oras', + description: 'Up to the minute news around the world.' + }) + + expect(results[37]).toMatchObject({ + start: '2023-06-27T21:50:00.000Z', + stop: '2023-06-28T00:00:00.000Z', + title: 'Eat Bulaga', + description: 'Rousing and engrossing segments with engaging hosts.' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + const result = parser({ + date, + content + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/superguidatv.it/superguidatv.it.config.js b/sites/superguidatv.it/superguidatv.it.config.js index b8d971423..6a89e9fe5 100644 --- a/sites/superguidatv.it/superguidatv.it.config.js +++ b/sites/superguidatv.it/superguidatv.it.config.js @@ -1,117 +1,117 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'superguidatv.it', - days: 3, - url({ channel, date }) { - let diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') - let day = { - 0: 'oggi', - 1: 'domani', - 2: 'dopodomani' - } - - return `https://www.superguidatv.it/programmazione-canale/${day[diff]}/guida-programmi-tv-${channel.site_id}/` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const providers = [ - '', - 'premium/', - 'sky-intrattenimento/', - 'sky-sport/', - 'sky-cinema/', - 'sky-doc-e-lifestyle/', - 'sky-news/', - 'sky-bambini/', - 'sky-musica/', - 'sky-primafila/', - 'dazn/', - 'rsi/' - ] - const promises = providers.map(p => axios.get(`https://www.superguidatv.it/canali/${p}`)) - - const channels = [] - await Promise.all(promises) - .then(responses => { - responses.forEach(r => { - const $ = cheerio.load(r.data) - - $('.sgtvchannellist_mainContainer .sgtvchannel_divCell a').each((i, link) => { - let [, site_id] = $(link) - .attr('href') - .match(/guida-programmi-tv-(.*)\/$/) || [null, null] - let name = $(link).find('.pchannel').text().trim() - - channels.push({ - lang: 'it', - site_id, - name - }) - }) - }) - }) - .catch(console.log) - - return channels - } -} - -function parseStart($item, date) { - const hours = $item('.sgtvchannelplan_hoursCell') - .clone() - .children('.sgtvOnairSpan') - .remove() - .end() - .text() - .trim() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${hours}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Rome' - }).toUTC() -} - -function parseTitle($item) { - return $item('.sgtvchannelplan_spanInfoNextSteps').text().trim() -} - -function parseCategory($item) { - const eventType = $item('.sgtvchannelplan_spanEventType').text().trim() - const [, category] = eventType.match(/(^[^(]+)/) || [null, ''] - - return category.trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.sgtvchannelplan_divContainer > .sgtvchannelplan_divTableRow') - .has('#containerInfoEvent') - .toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'superguidatv.it', + days: 3, + url({ channel, date }) { + let diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') + let day = { + 0: 'oggi', + 1: 'domani', + 2: 'dopodomani' + } + + return `https://www.superguidatv.it/programmazione-canale/${day[diff]}/guida-programmi-tv-${channel.site_id}/` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const providers = [ + '', + 'premium/', + 'sky-intrattenimento/', + 'sky-sport/', + 'sky-cinema/', + 'sky-doc-e-lifestyle/', + 'sky-news/', + 'sky-bambini/', + 'sky-musica/', + 'sky-primafila/', + 'dazn/', + 'rsi/' + ] + const promises = providers.map(p => axios.get(`https://www.superguidatv.it/canali/${p}`)) + + const channels = [] + await Promise.all(promises) + .then(responses => { + responses.forEach(r => { + const $ = cheerio.load(r.data) + + $('.sgtvchannellist_mainContainer .sgtvchannel_divCell a').each((i, link) => { + let [, site_id] = $(link) + .attr('href') + .match(/guida-programmi-tv-(.*)\/$/) || [null, null] + let name = $(link).find('.pchannel').text().trim() + + channels.push({ + lang: 'it', + site_id, + name + }) + }) + }) + }) + .catch(console.log) + + return channels + } +} + +function parseStart($item, date) { + const hours = $item('.sgtvchannelplan_hoursCell') + .clone() + .children('.sgtvOnairSpan') + .remove() + .end() + .text() + .trim() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${hours}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Rome' + }).toUTC() +} + +function parseTitle($item) { + return $item('.sgtvchannelplan_spanInfoNextSteps').text().trim() +} + +function parseCategory($item) { + const eventType = $item('.sgtvchannelplan_spanEventType').text().trim() + const [, category] = eventType.match(/(^[^(]+)/) || [null, ''] + + return category.trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.sgtvchannelplan_divContainer > .sgtvchannelplan_divTableRow') + .has('#containerInfoEvent') + .toArray() +} diff --git a/sites/superguidatv.it/superguidatv.it.test.js b/sites/superguidatv.it/superguidatv.it.test.js index 38f32eda1..0f8585e85 100644 --- a/sites/superguidatv.it/superguidatv.it.test.js +++ b/sites/superguidatv.it/superguidatv.it.test.js @@ -1,67 +1,67 @@ -// npm run channels:parse -- --config=sites/superguidatv.it/superguidatv.it.config.js --output=sites/superguidatv.it/superguidatv.it.channels.xml -// npm run grab -- --site=superguidatv.it - -const { parser, url } = require('./superguidatv.it.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'virgin-radio/461', - xmltv_id: 'VirginRadioTV.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date: dayjs.utc().startOf('d') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/oggi/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can generate valid url for tomorrow', () => { - expect(url({ channel, date: dayjs.utc().startOf('d').add(1, 'd') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/domani/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can generate valid url for after tomorrow', () => { - expect(url({ channel, date: dayjs.utc().startOf('d').add(2, 'd') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/dopodomani/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-11T01:00:00.000Z', - stop: '2023-01-11T05:00:00.000Z', - title: 'All Nite Rock', - category: 'Musica' - }) - - expect(results[13]).toMatchObject({ - start: '2023-01-12T05:00:00.000Z', - stop: '2023-01-12T05:30:00.000Z', - title: 'Free Rock', - category: 'Musica' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/superguidatv.it/superguidatv.it.config.js --output=sites/superguidatv.it/superguidatv.it.channels.xml +// npm run grab -- --site=superguidatv.it + +const { parser, url } = require('./superguidatv.it.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'virgin-radio/461', + xmltv_id: 'VirginRadioTV.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date: dayjs.utc().startOf('d') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/oggi/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can generate valid url for tomorrow', () => { + expect(url({ channel, date: dayjs.utc().startOf('d').add(1, 'd') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/domani/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can generate valid url for after tomorrow', () => { + expect(url({ channel, date: dayjs.utc().startOf('d').add(2, 'd') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/dopodomani/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-11T01:00:00.000Z', + stop: '2023-01-11T05:00:00.000Z', + title: 'All Nite Rock', + category: 'Musica' + }) + + expect(results[13]).toMatchObject({ + start: '2023-01-12T05:00:00.000Z', + stop: '2023-01-12T05:30:00.000Z', + title: 'Free Rock', + category: 'Musica' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/taiwanplus.com/taiwanplus.com.config.js b/sites/taiwanplus.com/taiwanplus.com.config.js index 94ed17aef..745cae588 100644 --- a/sites/taiwanplus.com/taiwanplus.com.config.js +++ b/sites/taiwanplus.com/taiwanplus.com.config.js @@ -1,73 +1,73 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const isSameOrAfter = require('dayjs/plugin/isSameOrAfter') -const isSameOrBefore = require('dayjs/plugin/isSameOrBefore') - -dayjs.extend(utc) -dayjs.extend(isSameOrAfter) -dayjs.extend(isSameOrBefore) - -module.exports = { - site: 'taiwanplus.com', - days: 7, - output: 'taiwanplus.com.guide.xml', - channels: 'taiwanplus.com.channels.xml', - lang: 'en', - delay: 5000, - - url: function () { - return 'https://www.taiwanplus.com/api/video/live/schedule/0' - }, - - request: { - method: 'GET', - timeout: 5000, - cache: { ttl: 60 * 60 * 1000 }, // 60 * 60 seconds = 1 hour - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - } - }, - - logo: function (context) { - return context.channel.logo - }, - - parser: function (context) { - const programs = [] - const scheduleDates = parseItems(context.content) - const today = dayjs.utc(context.date).startOf('day') - - for (let scheduleDate of scheduleDates) { - const currentScheduleDate = new dayjs.utc(scheduleDate.date, 'YYYY/MM/DD') - - if (currentScheduleDate.isSame(today)) { - scheduleDate.schedule.forEach(function (program, i) { - programs.push({ - title: program.title, - start: dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm'), - stop: - i != scheduleDate.schedule.length - 1 - ? dayjs.utc(scheduleDate.schedule[i + 1].dateTime, 'YYYY/MM/DD HH:mm') - : dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm').add(1, 'day').startOf('day'), - description: program.description, - icon: program.image, - category: program.categoryName, - rating: program.ageRating - }) - }) - } - } - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.data || !Array.isArray(data.data) ? [] : data.data - } else { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const isSameOrAfter = require('dayjs/plugin/isSameOrAfter') +const isSameOrBefore = require('dayjs/plugin/isSameOrBefore') + +dayjs.extend(utc) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) + +module.exports = { + site: 'taiwanplus.com', + days: 7, + output: 'taiwanplus.com.guide.xml', + channels: 'taiwanplus.com.channels.xml', + lang: 'en', + delay: 5000, + + url: function () { + return 'https://www.taiwanplus.com/api/video/live/schedule/0' + }, + + request: { + method: 'GET', + timeout: 5000, + cache: { ttl: 60 * 60 * 1000 }, // 60 * 60 seconds = 1 hour + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + } + }, + + logo: function (context) { + return context.channel.logo + }, + + parser: function (context) { + const programs = [] + const scheduleDates = parseItems(context.content) + const today = dayjs.utc(context.date).startOf('day') + + for (let scheduleDate of scheduleDates) { + const currentScheduleDate = new dayjs.utc(scheduleDate.date, 'YYYY/MM/DD') + + if (currentScheduleDate.isSame(today)) { + scheduleDate.schedule.forEach(function (program, i) { + programs.push({ + title: program.title, + start: dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm'), + stop: + i != scheduleDate.schedule.length - 1 + ? dayjs.utc(scheduleDate.schedule[i + 1].dateTime, 'YYYY/MM/DD HH:mm') + : dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm').add(1, 'day').startOf('day'), + description: program.description, + icon: program.image, + category: program.categoryName, + rating: program.ageRating + }) + }) + } + } + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.data || !Array.isArray(data.data) ? [] : data.data + } else { + return [] + } +} diff --git a/sites/taiwanplus.com/taiwanplus.com.test.js b/sites/taiwanplus.com/taiwanplus.com.test.js index a38c74c02..dc7142279 100644 --- a/sites/taiwanplus.com/taiwanplus.com.test.js +++ b/sites/taiwanplus.com/taiwanplus.com.test.js @@ -1,45 +1,45 @@ -// npm run grab -- --site=taiwanplus.com -// npx jest taiwanplus.com.test.js - -const { url, parser } = require('./taiwanplus.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-08-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'TaiwanPlusTV.tw', - lang: 'en', - logo: 'https://i.imgur.com/SfcZyqm.png' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.taiwanplus.com/api/video/live/schedule/0') -}) - -it('can parse response', () => { - const content = - '{"data":[{"date":"2023/08/20","weekday":"SUN","schedule":[{"programId":30668,"dateTime":"2023/08/20 00:00","time":"00:00","image":"https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp","title":"Master Class","shortDescription":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","description":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","ageRating":"0+","programWebSiteType":"4","url":"","vodId":null,"categoryId":90000474,"categoryType":2,"categoryName":"TaiwanPlus ✕ Discovery","categoryFullPath":"Originals/TaiwanPlus ✕ Discovery","encodedCategoryFullPath":"originals/taiwanplus-discovery"}]}],"success":true,"code":"0000","message":""}' - - const results = parser({ content, date }) - - expect(results).toMatchObject([ - { - title: 'Master Class', - start: dayjs.utc('2023/08/20 00:00', 'YYYY/MM/DD HH:mm'), - stop: dayjs.utc('2023/08/21 00:00', 'YYYY/MM/DD HH:mm'), - description: - 'From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.', - icon: 'https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp', - category: 'TaiwanPlus ✕ Discovery', - rating: '0+' - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=taiwanplus.com +// npx jest taiwanplus.com.test.js + +const { url, parser } = require('./taiwanplus.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-08-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'TaiwanPlusTV.tw', + lang: 'en', + logo: 'https://i.imgur.com/SfcZyqm.png' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.taiwanplus.com/api/video/live/schedule/0') +}) + +it('can parse response', () => { + const content = + '{"data":[{"date":"2023/08/20","weekday":"SUN","schedule":[{"programId":30668,"dateTime":"2023/08/20 00:00","time":"00:00","image":"https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp","title":"Master Class","shortDescription":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","description":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","ageRating":"0+","programWebSiteType":"4","url":"","vodId":null,"categoryId":90000474,"categoryType":2,"categoryName":"TaiwanPlus ✕ Discovery","categoryFullPath":"Originals/TaiwanPlus ✕ Discovery","encodedCategoryFullPath":"originals/taiwanplus-discovery"}]}],"success":true,"code":"0000","message":""}' + + const results = parser({ content, date }) + + expect(results).toMatchObject([ + { + title: 'Master Class', + start: dayjs.utc('2023/08/20 00:00', 'YYYY/MM/DD HH:mm'), + stop: dayjs.utc('2023/08/21 00:00', 'YYYY/MM/DD HH:mm'), + description: + 'From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.', + icon: 'https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp', + category: 'TaiwanPlus ✕ Discovery', + rating: '0+' + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tapdmv.com/tapdmv.com.config.js b/sites/tapdmv.com/tapdmv.com.config.js index 1424a82c3..388ac6573 100644 --- a/sites/tapdmv.com/tapdmv.com.config.js +++ b/sites/tapdmv.com/tapdmv.com.config.js @@ -1,61 +1,61 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tapdmv.com', - days: 2, - url({ channel, date }) { - return `https://epg.tapdmv.com/calendar/${ - channel.site_id - }?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=${date.toJSON()}&end=${date - .add(1, 'd') - .toJSON()}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - programs.push({ - title: item.program.trim(), - description: item.description, - category: item.genre, - icon: item.thumbnailImage, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://epg.tapdmv.com/calendar?$limit=10000&$sort[createdAt]=-1') - .then(r => r.data.data) - .catch(console.log) - - return items.map(item => { - const [, name] = item.name.match(/epg-tapgo-([^.]+).json/) - return { - site_id: item.id, - name - } - }) - } -} - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseItems(content, date) { - if (!content) return [] - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - const d = date.format('YYYY-MM-DD') - - return data.filter(i => i.startTime.includes(d)) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tapdmv.com', + days: 2, + url({ channel, date }) { + return `https://epg.tapdmv.com/calendar/${ + channel.site_id + }?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=${date.toJSON()}&end=${date + .add(1, 'd') + .toJSON()}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + programs.push({ + title: item.program.trim(), + description: item.description, + category: item.genre, + icon: item.thumbnailImage, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://epg.tapdmv.com/calendar?$limit=10000&$sort[createdAt]=-1') + .then(r => r.data.data) + .catch(console.log) + + return items.map(item => { + const [, name] = item.name.match(/epg-tapgo-([^.]+).json/) + return { + site_id: item.id, + name + } + }) + } +} + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseItems(content, date) { + if (!content) return [] + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + const d = date.format('YYYY-MM-DD') + + return data.filter(i => i.startTime.includes(d)) +} diff --git a/sites/tapdmv.com/tapdmv.com.test.js b/sites/tapdmv.com/tapdmv.com.test.js index f5602d96a..27f05d64e 100644 --- a/sites/tapdmv.com/tapdmv.com.test.js +++ b/sites/tapdmv.com/tapdmv.com.test.js @@ -1,51 +1,51 @@ -// npm run grab -- --site=tapdmv.com -// npm run channels:parse -- --config=./sites/tapdmv.com/tapdmv.com.config.js --output=./sites/tapdmv.com/tapdmv.com.channels.xml - -const { parser, url } = require('./tapdmv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756', - xmltv_id: 'TAPActionFlix.ph' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epg.tapdmv.com/calendar/94b7db9b-5bbd-47d3-a2d3-ce792342a756?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=2022-10-04T00:00:00.000Z&end=2022-10-05T00:00:00.000Z' - ) -}) - -it('can parse response', () => { - const content = - '[{"id":"0afc3cc0-eab8-4960-a8b5-55d76edeb8f0","program":"The Bourne Ultimatum","episode":"The Bourne Ultimatum","description":"Jason Bourne dodges a ruthless C.I.A. official and his Agents from a new assassination program while searching for the origins of his life as a trained killer.","genre":"Action","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-03T23:05:00.000Z","endTime":"2022-10-04T01:00:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:10.586Z","updatedAt":"2022-09-30T13:02:10.586Z"},{"id":"8dccd5e0-ab88-44b6-a2af-18d31c6e9ed7","program":"The Devil Inside ","episode":"The Devil Inside ","description":"In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.","genre":"Horror","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-04T01:00:00.000Z","endTime":"2022-10-04T02:25:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:24.031Z","updatedAt":"2022-09-30T13:02:24.031Z"}]' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-04T01:00:00.000Z', - stop: '2022-10-04T02:25:00.000Z', - title: 'The Devil Inside', - description: - 'In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.', - category: 'Horror', - icon: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tapdmv.com +// npm run channels:parse -- --config=./sites/tapdmv.com/tapdmv.com.config.js --output=./sites/tapdmv.com/tapdmv.com.channels.xml + +const { parser, url } = require('./tapdmv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756', + xmltv_id: 'TAPActionFlix.ph' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epg.tapdmv.com/calendar/94b7db9b-5bbd-47d3-a2d3-ce792342a756?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=2022-10-04T00:00:00.000Z&end=2022-10-05T00:00:00.000Z' + ) +}) + +it('can parse response', () => { + const content = + '[{"id":"0afc3cc0-eab8-4960-a8b5-55d76edeb8f0","program":"The Bourne Ultimatum","episode":"The Bourne Ultimatum","description":"Jason Bourne dodges a ruthless C.I.A. official and his Agents from a new assassination program while searching for the origins of his life as a trained killer.","genre":"Action","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-03T23:05:00.000Z","endTime":"2022-10-04T01:00:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:10.586Z","updatedAt":"2022-09-30T13:02:10.586Z"},{"id":"8dccd5e0-ab88-44b6-a2af-18d31c6e9ed7","program":"The Devil Inside ","episode":"The Devil Inside ","description":"In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.","genre":"Horror","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-04T01:00:00.000Z","endTime":"2022-10-04T02:25:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:24.031Z","updatedAt":"2022-09-30T13:02:24.031Z"}]' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-04T01:00:00.000Z', + stop: '2022-10-04T02:25:00.000Z', + title: 'The Devil Inside', + description: + 'In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.', + category: 'Horror', + icon: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/telecablesat.fr/telecablesat.fr.config.js b/sites/telecablesat.fr/telecablesat.fr.config.js index 6a81cb725..f3ef396e4 100644 --- a/sites/telecablesat.fr/telecablesat.fr.config.js +++ b/sites/telecablesat.fr/telecablesat.fr.config.js @@ -1,107 +1,107 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -const API_ENDPOINT = 'https://tv-programme.telecablesat.fr/chaine' - -module.exports = { - site: 'telecablesat.fr', - days: 2, - delay: 5000, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/${channel.site_id}/index.html?date=${date.format('YYYY-MM-DD')}` - }, - async parser({ content, date, channel }) { - let programs = [] - let items = parseItems(content) - if (!items.length) return programs - const url = `${API_ENDPOINT}/${channel.site_id}/index.html` - const promises = [ - axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=noon`), - axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=afternoon`) - ] - await Promise.allSettled(promises).then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data)) - } - }) - }) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://tv-programme.telecablesat.fr/') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - const items = $( - '#ptgv_left > section.main > div > div > div:nth-child(1) > div > div > div.linker.with_search > div.inside > div.scroller > a' - ).toArray() - - return items.map(item => { - const $item = cheerio.load(item) - const link = $item('*').attr('href') - const [, site_id] = link.match(/\/chaine\/(\d+)\//) || [null, null] - const name = $item('*').text().trim() - return { - lang: 'fr', - site_id, - name - } - }) - } -} - -function parseStart($item, date) { - const timeString = $item('.schedule-hour').text() - if (!timeString) return null - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${timeString}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Paris' - }).toUTC() -} - -function parseIcon($item) { - const imgSrc = $item('img').attr('src') - - return imgSrc ? `https:${imgSrc}` : null -} - -function parseTitle($item) { - return $item('div.item-content > div.title-left').text().trim() -} - -function parseDescription($item) { - return $item('div.item-content > p').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - '#ptgv_left > div.container > div.row.no-gutter > div.col-md-8 > div > div > div > div > div > div > div.news' - ).toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +const API_ENDPOINT = 'https://tv-programme.telecablesat.fr/chaine' + +module.exports = { + site: 'telecablesat.fr', + days: 2, + delay: 5000, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/${channel.site_id}/index.html?date=${date.format('YYYY-MM-DD')}` + }, + async parser({ content, date, channel }) { + let programs = [] + let items = parseItems(content) + if (!items.length) return programs + const url = `${API_ENDPOINT}/${channel.site_id}/index.html` + const promises = [ + axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=noon`), + axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=afternoon`) + ] + await Promise.allSettled(promises).then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data)) + } + }) + }) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://tv-programme.telecablesat.fr/') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + const items = $( + '#ptgv_left > section.main > div > div > div:nth-child(1) > div > div > div.linker.with_search > div.inside > div.scroller > a' + ).toArray() + + return items.map(item => { + const $item = cheerio.load(item) + const link = $item('*').attr('href') + const [, site_id] = link.match(/\/chaine\/(\d+)\//) || [null, null] + const name = $item('*').text().trim() + return { + lang: 'fr', + site_id, + name + } + }) + } +} + +function parseStart($item, date) { + const timeString = $item('.schedule-hour').text() + if (!timeString) return null + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${timeString}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Paris' + }).toUTC() +} + +function parseIcon($item) { + const imgSrc = $item('img').attr('src') + + return imgSrc ? `https:${imgSrc}` : null +} + +function parseTitle($item) { + return $item('div.item-content > div.title-left').text().trim() +} + +function parseDescription($item) { + return $item('div.item-content > p').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + '#ptgv_left > div.container > div.row.no-gutter > div.col-md-8 > div > div > div > div > div > div > div.news' + ).toArray() +} diff --git a/sites/telecablesat.fr/telecablesat.fr.test.js b/sites/telecablesat.fr/telecablesat.fr.test.js index b6ec40a8b..47f1db5ff 100644 --- a/sites/telecablesat.fr/telecablesat.fr.test.js +++ b/sites/telecablesat.fr/telecablesat.fr.test.js @@ -1,116 +1,116 @@ -// npm run channels:parse -- --config=./sites/telecablesat.fr/telecablesat.fr.config.js --output=./sites/telecablesat.fr/telecablesat.fr.channels.xml -// npm run grab -- --site=telecablesat.fr - -const { parser, url } = require('./telecablesat.fr.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: '13emeRue.fr' -} - -jest.mock('axios') - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11' - ) -}) - -it('can parse response', done => { - const content = - '
    06:25

    Des randonneurs font une macabre découverte en installant leur tente dans la nature : une jeune fille de Lusagne a en effet été sauvagement assassinée et son corps a été dissimulé sommairement dans les buissons. Rapidement, des lettres anonymes...

    08:05

    Alors que les fêtes de fin d\'année battent leur plein, le commissaire Rousseau se voit confronté à une délicate affaire. En peu de temps, une troisième jeune fille vient d\'être retrouvée assassinée. Le vieux limier ne croit pas à l\'hypothèse...

    ' - - axios.get.mockImplementation(url => { - if ( - url === 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11&period=noon' - ) { - return Promise.resolve({ - data: '
    12:35

    Brett, Foster et Kidd font une virée dans l\'Indiana. Sur la route, ils sont les témoins d\'un accident. Un bus qui transporte une équipe de hockey percute une voiture en panne. Sans réseau téléphonique, ils ne peuvent prévenir leurs collègues. En...

    ' - }) - } else if ( - url === - 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11&period=afternoon' - ) { - return Promise.resolve({ - data: '
    01:45

    Lors d\'une urgence, la vie du lieutenant Casey est soudainement mise en danger : un homme le menace avec une arme. Grissom prévient la caserne qu\'ils devront se préparer à une évaluation de leur performance. Otis apprend qu\'il a remporté un prix...

    05:05

    Nos programmes se terminent pour cette journée, en attendant ceux de demain.

    ' - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, date, channel }) - .then(result => { - result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-11T05:25:00.000Z', - stop: '2022-03-11T07:05:00.000Z', - title: 'Alex Hugo', - description: - 'Des randonneurs font une macabre découverte en installant leur tente dans la nature : une jeune fille de Lusagne a en effet été sauvagement assassinée et son corps a été dissimulé sommairement dans les buissons. Rapidement, des lettres anonymes...', - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3C2pQvC0r6FCyxLq6aUFEyMxr_rvGA-IobGYhFb77UtXnq516FuRz_aQKmM4f08mdPKY-HuJR2lh1vh02RHABRSnukK0-wYMwBERBei1rOXUwBjOc2oCSgbIqUQ0KrILqNh5VwEl9iO97qKi9hDe_wf1uJLOyO-Pw.jpg' - }, - { - start: '2022-03-11T07:05:00.000Z', - stop: '2022-03-11T11:35:00.000Z', - title: 'Les saisons meurtrières : hiver rouge', - description: - "Alors que les fêtes de fin d'année battent leur plein, le commissaire Rousseau se voit confronté à une délicate affaire. En peu de temps, une troisième jeune fille vient d'être retrouvée assassinée. Le vieux limier ne croit pas à l'hypothèse...", - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDoMgEEDRu7Cu4AgIeBY2DAzaNiIRmjRteve6_Mlb_C_D0IgtbOu9tsULL_Y61O3oRxtiKjx8XidRWokX6l7Eo_RwL3TqbCNADCm7qGB0lkwGADTGBpTSeXEBbeyk1EguAybptJbyapyQAmogG9VMwB91ZTe2h_a8PubZvKXV7PcH.jpg' - }, - { - start: '2022-03-11T11:35:00.000Z', - stop: '2022-03-12T00:45:00.000Z', - title: 'Chicago Fire', - description: - "Brett, Foster et Kidd font une virée dans l'Indiana. Sur la route, ils sont les témoins d'un accident. Un bus qui transporte une équipe de hockey percute une voiture en panne. Sans réseau téléphonique, ils ne peuvent prévenir leurs collègues. En...", - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3DWwpayQN_CZaFLq6aUFEyMxr_rvGA-IlJjMYut99rmIIPc67VuRz_aNS1loPfzZF5WHgr3INNROt0Knya7BJBoyT5NoLxjmwEgWusoau2DtCqBc1OOiCZNI2vvVLSAigzl0WSvAVFHPdzrKi5ip_b4PxDtSzsjvj8.jpg' - }, - { - start: '2022-03-12T00:45:00.000Z', - stop: '2022-03-12T04:05:00.000Z', - title: 'Chicago Fire', - description: - "Lors d'une urgence, la vie du lieutenant Casey est soudainement mise en danger : un homme le menace avec une arme. Grissom prévient la caserne qu'ils devront se préparer à une évaluation de leur performance. Otis apprend qu'il a remporté un prix...", - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3C2hZVSoG_hsixLq6aUFEyMxr_rvGA-ImJjsYit99qWIIPc61C3ox9toFRGfD9P5rTyWLgHSUfpeCt8muwIgDBlTxMo79hmAIjWOoxa-yCT9loxMqIBjhMb7eFqk8qTy6CJZ01GUTTjva7iInZsj_9jnu1LOyO-Pw.jpg' - }, - { - start: '2022-03-12T04:05:00.000Z', - stop: '2022-03-12T05:05:00.000Z', - title: 'Fin des programmes', - description: - 'Nos programmes se terminent pour cette journée, en attendant ceux de demain.', - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5LDsIgEADQu7C2hXEKTHsWNsOvVVNKCiZG4931neB9hOeWxCK23mtbnHRyr0Pdjn60IcQy8vt5phTXNJbUnQxH6Xwr6dSZAkDgmOcwgZop2QwA3lpijzg7yUZ7iHMgUhRMRONz1IioMKvAekpwhYkJx3tdxUXs3B7_hzH2haTF9wc.jpg' - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: - ' ', - date, - channel - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run channels:parse -- --config=./sites/telecablesat.fr/telecablesat.fr.config.js --output=./sites/telecablesat.fr/telecablesat.fr.channels.xml +// npm run grab -- --site=telecablesat.fr + +const { parser, url } = require('./telecablesat.fr.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: '13emeRue.fr' +} + +jest.mock('axios') + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11' + ) +}) + +it('can parse response', done => { + const content = + '
    06:25

    Des randonneurs font une macabre découverte en installant leur tente dans la nature : une jeune fille de Lusagne a en effet été sauvagement assassinée et son corps a été dissimulé sommairement dans les buissons. Rapidement, des lettres anonymes...

    08:05

    Alors que les fêtes de fin d\'année battent leur plein, le commissaire Rousseau se voit confronté à une délicate affaire. En peu de temps, une troisième jeune fille vient d\'être retrouvée assassinée. Le vieux limier ne croit pas à l\'hypothèse...

    ' + + axios.get.mockImplementation(url => { + if ( + url === 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11&period=noon' + ) { + return Promise.resolve({ + data: '
    12:35

    Brett, Foster et Kidd font une virée dans l\'Indiana. Sur la route, ils sont les témoins d\'un accident. Un bus qui transporte une équipe de hockey percute une voiture en panne. Sans réseau téléphonique, ils ne peuvent prévenir leurs collègues. En...

    ' + }) + } else if ( + url === + 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11&period=afternoon' + ) { + return Promise.resolve({ + data: '
    01:45

    Lors d\'une urgence, la vie du lieutenant Casey est soudainement mise en danger : un homme le menace avec une arme. Grissom prévient la caserne qu\'ils devront se préparer à une évaluation de leur performance. Otis apprend qu\'il a remporté un prix...

    05:05

    Nos programmes se terminent pour cette journée, en attendant ceux de demain.

    ' + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, date, channel }) + .then(result => { + result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-11T05:25:00.000Z', + stop: '2022-03-11T07:05:00.000Z', + title: 'Alex Hugo', + description: + 'Des randonneurs font une macabre découverte en installant leur tente dans la nature : une jeune fille de Lusagne a en effet été sauvagement assassinée et son corps a été dissimulé sommairement dans les buissons. Rapidement, des lettres anonymes...', + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3C2pQvC0r6FCyxLq6aUFEyMxr_rvGA-IobGYhFb77UtXnq516FuRz_aQKmM4f08mdPKY-HuJR2lh1vh02RHABRSnukK0-wYMwBERBei1rOXUwBjOc2oCSgbIqUQ0KrILqNh5VwEl9iO97qKi9hDe_wf1uJLOyO-Pw.jpg' + }, + { + start: '2022-03-11T07:05:00.000Z', + stop: '2022-03-11T11:35:00.000Z', + title: 'Les saisons meurtrières : hiver rouge', + description: + "Alors que les fêtes de fin d'année battent leur plein, le commissaire Rousseau se voit confronté à une délicate affaire. En peu de temps, une troisième jeune fille vient d'être retrouvée assassinée. Le vieux limier ne croit pas à l'hypothèse...", + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDoMgEEDRu7Cu4AgIeBY2DAzaNiIRmjRteve6_Mlb_C_D0IgtbOu9tsULL_Y61O3oRxtiKjx8XidRWokX6l7Eo_RwL3TqbCNADCm7qGB0lkwGADTGBpTSeXEBbeyk1EguAybptJbyapyQAmogG9VMwB91ZTe2h_a8PubZvKXV7PcH.jpg' + }, + { + start: '2022-03-11T11:35:00.000Z', + stop: '2022-03-12T00:45:00.000Z', + title: 'Chicago Fire', + description: + "Brett, Foster et Kidd font une virée dans l'Indiana. Sur la route, ils sont les témoins d'un accident. Un bus qui transporte une équipe de hockey percute une voiture en panne. Sans réseau téléphonique, ils ne peuvent prévenir leurs collègues. En...", + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3DWwpayQN_CZaFLq6aUFEyMxr_rvGA-IlJjMYut99rmIIPc67VuRz_aNS1loPfzZF5WHgr3INNROt0Knya7BJBoyT5NoLxjmwEgWusoau2DtCqBc1OOiCZNI2vvVLSAigzl0WSvAVFHPdzrKi5ip_b4PxDtSzsjvj8.jpg' + }, + { + start: '2022-03-12T00:45:00.000Z', + stop: '2022-03-12T04:05:00.000Z', + title: 'Chicago Fire', + description: + "Lors d'une urgence, la vie du lieutenant Casey est soudainement mise en danger : un homme le menace avec une arme. Grissom prévient la caserne qu'ils devront se préparer à une évaluation de leur performance. Otis apprend qu'il a remporté un prix...", + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3C2hZVSoG_hsixLq6aUFEyMxr_rvGA-ImJjsYit99qWIIPc61C3ox9toFRGfD9P5rTyWLgHSUfpeCt8muwIgDBlTxMo79hmAIjWOoxa-yCT9loxMqIBjhMb7eFqk8qTy6CJZ01GUTTjva7iInZsj_9jnu1LOyO-Pw.jpg' + }, + { + start: '2022-03-12T04:05:00.000Z', + stop: '2022-03-12T05:05:00.000Z', + title: 'Fin des programmes', + description: + 'Nos programmes se terminent pour cette journée, en attendant ceux de demain.', + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5LDsIgEADQu7C2hXEKTHsWNsOvVVNKCiZG4931neB9hOeWxCK23mtbnHRyr0Pdjn60IcQy8vt5phTXNJbUnQxH6Xwr6dSZAkDgmOcwgZop2QwA3lpijzg7yUZ7iHMgUhRMRONz1IioMKvAekpwhYkJx3tdxUXs3B7_hzH2haTF9wc.jpg' + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: + ' ', + date, + channel + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/telenet.tv/telenet.tv.config.js b/sites/telenet.tv/telenet.tv.config.js index e9dde5d30..38d5a57f0 100644 --- a/sites/telenet.tv/telenet.tv.config.js +++ b/sites/telenet.tv/telenet.tv.config.js @@ -1,132 +1,132 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' -const API_PROD_ENDPOINT = 'https://prod.spark.telenet.tv/eng/web/linear-service/v2' - -module.exports = { - site: 'telenet.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - return `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const promises = [ - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(6, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(12, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(18, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ) - ] - - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - const parsed = parseItems(r.value.data, channel) - - items = items.concat(parsed) - } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item, channel) - programs.push({ - title: item.title, - description: detail.longDescription, - category: detail.genres, - actors: detail.actors, - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_PROD_ENDPOINT}/channels?cityId=65535&language=en&productClass=Orion-DASH`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'be', - site_id: item.id, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item, channel) { - if (!item.id) return {} - const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.entries)) return [] - const channelData = data.entries.find(e => e.channelId === channel.site_id) - if (!channelData) return [] - - return Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(detail) { - if (!detail.seasonNumber) return null - if (String(detail.seasonNumber).length > 2) return null - return detail.seasonNumber -} - -function parseEpisode(detail) { - if (!detail.episodeNumber) return null - if (String(detail.episodeNumber).length > 3) return null - return detail.episodeNumber -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' +const API_PROD_ENDPOINT = 'https://prod.spark.telenet.tv/eng/web/linear-service/v2' + +module.exports = { + site: 'telenet.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + return `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const promises = [ + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(6, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(12, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(18, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ) + ] + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item, channel) + programs.push({ + title: item.title, + description: detail.longDescription, + category: detail.genres, + actors: detail.actors, + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_PROD_ENDPOINT}/channels?cityId=65535&language=en&productClass=Orion-DASH`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'be', + site_id: item.id, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item, channel) { + if (!item.id) return {} + const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.entries)) return [] + const channelData = data.entries.find(e => e.channelId === channel.site_id) + if (!channelData) return [] + + return Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(detail) { + if (!detail.seasonNumber) return null + if (String(detail.seasonNumber).length > 2) return null + return detail.seasonNumber +} + +function parseEpisode(detail) { + if (!detail.episodeNumber) return null + if (String(detail.episodeNumber).length > 3) return null + return detail.episodeNumber +} diff --git a/sites/telenet.tv/telenet.tv.test.js b/sites/telenet.tv/telenet.tv.test.js index 4202e27d7..ccaede562 100644 --- a/sites/telenet.tv/telenet.tv.test.js +++ b/sites/telenet.tv/telenet.tv.test.js @@ -1,91 +1,91 @@ -// npm run channels:parse -- --config=./sites/telenet.tv/telenet.tv.config.js --output=./sites/telenet.tv/telenet.tv.channels.xml -// npm run grab -- --site=telenet.tv - -const { parser, url } = require('./telenet.tv.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' -const API_PROD_ENDPOINT = 'https://prod.spark.telenet.tv/eng/web/linear-service/v2' - -jest.mock('axios') - -const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'outtv', - xmltv_id: 'OutTV.nl', - lang: 'nl' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe(`${API_STATIC_ENDPOINT}/nl/events/segments/20221030000000`) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030060000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030120000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030180000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) - }) - } else if ( - url === - `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2459095~~2FEP036477800004,imi:0a2f4207b03c16c70b7fb3be8e07881aafe44106?returnLinearContent=true&language=nl` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T23:56:00.000Z', - stop: '2022-10-30T01:44:00.000Z', - title: 'Queer as Folk USA', - description: - "Justin belandt in de gevangenis, Brian en Brandon banen zich een weg door de lijst, Ben treurt, Melanie en Lindsay proberen een interne scheiding en Emmett's stalker onthult zichzelf.", - category: ['Dramaserie', 'LHBTI'], - actors: [ - 'Gale Harold', - 'Hal Sparks', - 'Randy Harrison', - 'Peter Paige', - 'Scott Lowell', - 'Thea Gill', - 'Michelle Clunie', - 'Sharon Gless' - ], - season: 5, - episode: 8 - }) -}) - -it('can handle empty guide', async () => { - let results = await parser({ content: '', channel, date }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/telenet.tv/telenet.tv.config.js --output=./sites/telenet.tv/telenet.tv.channels.xml +// npm run grab -- --site=telenet.tv + +const { parser, url } = require('./telenet.tv.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' +const API_PROD_ENDPOINT = 'https://prod.spark.telenet.tv/eng/web/linear-service/v2' + +jest.mock('axios') + +const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'outtv', + xmltv_id: 'OutTV.nl', + lang: 'nl' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe(`${API_STATIC_ENDPOINT}/nl/events/segments/20221030000000`) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030060000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030120000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030180000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) + }) + } else if ( + url === + `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2459095~~2FEP036477800004,imi:0a2f4207b03c16c70b7fb3be8e07881aafe44106?returnLinearContent=true&language=nl` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T23:56:00.000Z', + stop: '2022-10-30T01:44:00.000Z', + title: 'Queer as Folk USA', + description: + "Justin belandt in de gevangenis, Brian en Brandon banen zich een weg door de lijst, Ben treurt, Melanie en Lindsay proberen een interne scheiding en Emmett's stalker onthult zichzelf.", + category: ['Dramaserie', 'LHBTI'], + actors: [ + 'Gale Harold', + 'Hal Sparks', + 'Randy Harrison', + 'Peter Paige', + 'Scott Lowell', + 'Thea Gill', + 'Michelle Clunie', + 'Sharon Gless' + ], + season: 5, + episode: 8 + }) +}) + +it('can handle empty guide', async () => { + let results = await parser({ content: '', channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/teliatv.ee/teliatv.ee.config.js b/sites/teliatv.ee/teliatv.ee.config.js index 564066617..426b7fd30 100644 --- a/sites/teliatv.ee/teliatv.ee.config.js +++ b/sites/teliatv.ee/teliatv.ee.config.js @@ -1,67 +1,67 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'teliatv.ee', - days: 2, - url({ date, channel }) { - const [lang, channelId] = channel.site_id.split('#') - return `https://api.teliatv.ee/dtv-api/3.2/${lang}/epg/guide?channelIds=${channelId}&relations=programmes&images=webGuideItemLarge&startAt=${date - .add(1, 'd') - .format('YYYY-MM-DDTHH:mm')}&startAtOp=lte&endAt=${date.format( - 'YYYY-MM-DDTHH:mm' - )}&endAtOp=gt` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.name, - icon: parseIcon(item), - start: dayjs(item.startAt), - stop: dayjs(item.endAt) - }) - }) - - return programs - }, - async channels({ lang }) { - const data = await axios - .get(`https://api.teliatv.ee/dtv-api/3.0/${lang}/channel-lists?listClass=tv&ui=tv-web`) - .then(r => r.data) - .catch(console.log) - - return Object.values(data.channels).map(item => { - return { - lang, - site_id: item.id, - name: item.title - } - }) - } -} - -function parseIcon(item) { - return item.images.webGuideItemLarge - ? `https://inet-static.mw.elion.ee${item.images.webGuideItemLarge}` - : null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !data.relations || !data.categoryItems) return [] - const [, channelId] = channel.site_id.split('#') - const items = data.categoryItems[channelId] || [] - - return items - .map(i => { - const programmeId = i.related.programmeIds[0] - if (!programmeId) return null - const progData = data.relations.programmes[programmeId] - if (!progData) return null - - return { ...i, ...progData } - }) - .filter(i => i) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'teliatv.ee', + days: 2, + url({ date, channel }) { + const [lang, channelId] = channel.site_id.split('#') + return `https://api.teliatv.ee/dtv-api/3.2/${lang}/epg/guide?channelIds=${channelId}&relations=programmes&images=webGuideItemLarge&startAt=${date + .add(1, 'd') + .format('YYYY-MM-DDTHH:mm')}&startAtOp=lte&endAt=${date.format( + 'YYYY-MM-DDTHH:mm' + )}&endAtOp=gt` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.name, + icon: parseIcon(item), + start: dayjs(item.startAt), + stop: dayjs(item.endAt) + }) + }) + + return programs + }, + async channels({ lang }) { + const data = await axios + .get(`https://api.teliatv.ee/dtv-api/3.0/${lang}/channel-lists?listClass=tv&ui=tv-web`) + .then(r => r.data) + .catch(console.log) + + return Object.values(data.channels).map(item => { + return { + lang, + site_id: item.id, + name: item.title + } + }) + } +} + +function parseIcon(item) { + return item.images.webGuideItemLarge + ? `https://inet-static.mw.elion.ee${item.images.webGuideItemLarge}` + : null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !data.relations || !data.categoryItems) return [] + const [, channelId] = channel.site_id.split('#') + const items = data.categoryItems[channelId] || [] + + return items + .map(i => { + const programmeId = i.related.programmeIds[0] + if (!programmeId) return null + const progData = data.relations.programmes[programmeId] + if (!progData) return null + + return { ...i, ...progData } + }) + .filter(i => i) +} diff --git a/sites/teliatv.ee/teliatv.ee.test.js b/sites/teliatv.ee/teliatv.ee.test.js index 4c6ef11b5..a41622ef4 100644 --- a/sites/teliatv.ee/teliatv.ee.test.js +++ b/sites/teliatv.ee/teliatv.ee.test.js @@ -1,61 +1,61 @@ -// npm run channels:parse -- --config=./sites/teliatv.ee/teliatv.ee.config.js --output=./sites/teliatv.ee/teliatv.ee.channels.xml --set=lang:et -// npm run grab -- --site=teliatv.ee - -const { parser, url } = require('./teliatv.ee.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-20', 'YYYY-MM-DD').startOf('d') -const channel = { - lang: 'et', - site_id: 'et#1', - xmltv_id: 'ETV.ee' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://api.teliatv.ee/dtv-api/3.2/et/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' - ) -}) - -it('can generate valid url with different language', () => { - const ruChannel = { - lang: 'ru', - site_id: 'ru#1', - xmltv_id: 'ETV.ee' - } - expect(url({ date, channel: ruChannel })).toBe( - 'https://api.teliatv.ee/dtv-api/3.2/ru/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' - ) -}) - -it('can parse response', () => { - const content = - '{"categoryItems":{"1":[{"id":136227,"type":"epgSeries","name":"Inimjaht","originalName":"Manhunt","price":null,"owner":"ETV","ownerId":1,"images":{"webGuideItemLarge":"/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg"},"packetIds":[30,34,38,129,130,162,191,242,243,244,447,483,484,485,486],"related":{"programmeIds":[27224371]}}]},"relations":{"programmes":{"27224371":{"id":27224371,"startAt":"2021-11-20T00:05:00+02:00","endAt":"2021-11-20T00:55:00+02:00","publicTo":"2021-12-04T02:05:00+02:00","status":"default","channelId":1,"broadcastId":78248901,"hasMarkers":false,"catchup":false}}}}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-19T22:05:00.000Z', - stop: '2021-11-19T22:55:00.000Z', - title: 'Inimjaht', - icon: 'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"categoryItems":{},"relations":{}}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/teliatv.ee/teliatv.ee.config.js --output=./sites/teliatv.ee/teliatv.ee.channels.xml --set=lang:et +// npm run grab -- --site=teliatv.ee + +const { parser, url } = require('./teliatv.ee.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-20', 'YYYY-MM-DD').startOf('d') +const channel = { + lang: 'et', + site_id: 'et#1', + xmltv_id: 'ETV.ee' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://api.teliatv.ee/dtv-api/3.2/et/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' + ) +}) + +it('can generate valid url with different language', () => { + const ruChannel = { + lang: 'ru', + site_id: 'ru#1', + xmltv_id: 'ETV.ee' + } + expect(url({ date, channel: ruChannel })).toBe( + 'https://api.teliatv.ee/dtv-api/3.2/ru/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' + ) +}) + +it('can parse response', () => { + const content = + '{"categoryItems":{"1":[{"id":136227,"type":"epgSeries","name":"Inimjaht","originalName":"Manhunt","price":null,"owner":"ETV","ownerId":1,"images":{"webGuideItemLarge":"/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg"},"packetIds":[30,34,38,129,130,162,191,242,243,244,447,483,484,485,486],"related":{"programmeIds":[27224371]}}]},"relations":{"programmes":{"27224371":{"id":27224371,"startAt":"2021-11-20T00:05:00+02:00","endAt":"2021-11-20T00:55:00+02:00","publicTo":"2021-12-04T02:05:00+02:00","status":"default","channelId":1,"broadcastId":78248901,"hasMarkers":false,"catchup":false}}}}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-19T22:05:00.000Z', + stop: '2021-11-19T22:55:00.000Z', + title: 'Inimjaht', + icon: 'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"categoryItems":{},"relations":{}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/telkku.com/telkku.com.config.js b/sites/telkku.com/telkku.com.config.js index bd7fb8ae5..3ce3ac6be 100644 --- a/sites/telkku.com/telkku.com.config.js +++ b/sites/telkku.com/telkku.com.config.js @@ -1,72 +1,72 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'telkku.com', - days: 2, - url: function ({ channel, date }) { - const [group] = channel.site_id.split('#') - - return `https://telkku.com/api/channel-groups/${group}/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=${date.format( - 'YYYY-MM-DD' - )}&view=PublicationDetails` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = getItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - icon: getIcon(item), - start: getStart(item), - stop: getStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://telkku.com/api/channel-groups') - .then(r => r.data) - .catch(console.log) - - let items = [] - data.response.forEach(group => { - group.channels.forEach(channel => { - items.push({ - lang: 'fi', - site_id: `${group.id}#${channel.id}`, - name: channel.name - }) - }) - }) - - return items - } -} - -function getIcon(item) { - const image = item.images.find(i => i.type === 'default' && i.sizeTag === '1200x630') - - return image ? image.url : null -} - -function getStart(item) { - return dayjs(item.startTime) -} - -function getStop(item) { - return dayjs(item.endTime) -} - -function getItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !data.response || !Array.isArray(data.response.publicationsByChannel)) return [] - const channelData = data.response.publicationsByChannel.find(i => i.channel.id === channelId) - if (!channelData || !Array.isArray(channelData.publications)) return [] - - return channelData.publications -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'telkku.com', + days: 2, + url: function ({ channel, date }) { + const [group] = channel.site_id.split('#') + + return `https://telkku.com/api/channel-groups/${group}/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=${date.format( + 'YYYY-MM-DD' + )}&view=PublicationDetails` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = getItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + icon: getIcon(item), + start: getStart(item), + stop: getStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://telkku.com/api/channel-groups') + .then(r => r.data) + .catch(console.log) + + let items = [] + data.response.forEach(group => { + group.channels.forEach(channel => { + items.push({ + lang: 'fi', + site_id: `${group.id}#${channel.id}`, + name: channel.name + }) + }) + }) + + return items + } +} + +function getIcon(item) { + const image = item.images.find(i => i.type === 'default' && i.sizeTag === '1200x630') + + return image ? image.url : null +} + +function getStart(item) { + return dayjs(item.startTime) +} + +function getStop(item) { + return dayjs(item.endTime) +} + +function getItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !data.response || !Array.isArray(data.response.publicationsByChannel)) return [] + const channelData = data.response.publicationsByChannel.find(i => i.channel.id === channelId) + if (!channelData || !Array.isArray(channelData.publications)) return [] + + return channelData.publications +} diff --git a/sites/telkku.com/telkku.com.test.js b/sites/telkku.com/telkku.com.test.js index 2946fdc05..d03dca751 100644 --- a/sites/telkku.com/telkku.com.test.js +++ b/sites/telkku.com/telkku.com.test.js @@ -1,49 +1,49 @@ -// npm run channels:parse -- --config=./sites/telkku.com/telkku.com.config.js --output=./sites/telkku.com/telkku.com.channels.xml -// npm run grab -- --site=telkku.com - -const { parser, url } = require('./telkku.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'default_builtin_channelgroup1#yle-tv1', - xmltv_id: 'YleTV1.fi' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://telkku.com/api/channel-groups/default_builtin_channelgroup1/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=2022-10-29&view=PublicationDetails' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-28T20:50:00.000Z', - stop: '2022-10-28T21:20:00.000Z', - title: 'Puoli seitsemän', - description: - 'Vieraana näyttelijä Elias Salonen. Puoli seiskassa vietetään sekä halloweeniä että joulua, kun Olli-Pekka tapaa todellisen jouluttajan. Juontajina Anniina Valtonen, Tuulianna Tola ja Olli-Pekka Kursi.', - icon: 'https://thumbor.prod.telkku.com/YTglotoUl7aJtzPtYnvM9tH03sY=/1200x630/smart/filters:quality(86):format(jpeg)/img.prod.telkku.com/program-images/0f885238ac16ce167a9d80eace450254.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')), - channel - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/telkku.com/telkku.com.config.js --output=./sites/telkku.com/telkku.com.channels.xml +// npm run grab -- --site=telkku.com + +const { parser, url } = require('./telkku.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'default_builtin_channelgroup1#yle-tv1', + xmltv_id: 'YleTV1.fi' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://telkku.com/api/channel-groups/default_builtin_channelgroup1/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=2022-10-29&view=PublicationDetails' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-28T20:50:00.000Z', + stop: '2022-10-28T21:20:00.000Z', + title: 'Puoli seitsemän', + description: + 'Vieraana näyttelijä Elias Salonen. Puoli seiskassa vietetään sekä halloweeniä että joulua, kun Olli-Pekka tapaa todellisen jouluttajan. Juontajina Anniina Valtonen, Tuulianna Tola ja Olli-Pekka Kursi.', + icon: 'https://thumbor.prod.telkku.com/YTglotoUl7aJtzPtYnvM9tH03sY=/1200x630/smart/filters:quality(86):format(jpeg)/img.prod.telkku.com/program-images/0f885238ac16ce167a9d80eace450254.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')), + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/telkussa.fi/telkussa.fi.config.js b/sites/telkussa.fi/telkussa.fi.config.js index a887f4ca0..d059f2732 100644 --- a/sites/telkussa.fi/telkussa.fi.config.js +++ b/sites/telkussa.fi/telkussa.fi.config.js @@ -1,30 +1,30 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'telkussa.fi', - days: 2, - url: function ({ date, channel }) { - return `https://telkussa.fi/API/Channel/${channel.site_id}/${date.format('YYYYMMDD')}` - }, - parser: function ({ content }) { - const programs = [] - const items = JSON.parse(content) - if (!items.length) return programs - - items.forEach(item => { - if (item.name && item.start && item.stop) { - const start = dayjs.unix(parseInt(item.start) * 60) - const stop = dayjs.unix(parseInt(item.stop) * 60) - - programs.push({ - title: item.name, - description: item.description, - start: start.toString(), - stop: stop.toString() - }) - } - }) - - return programs - } -} +const dayjs = require('dayjs') + +module.exports = { + site: 'telkussa.fi', + days: 2, + url: function ({ date, channel }) { + return `https://telkussa.fi/API/Channel/${channel.site_id}/${date.format('YYYYMMDD')}` + }, + parser: function ({ content }) { + const programs = [] + const items = JSON.parse(content) + if (!items.length) return programs + + items.forEach(item => { + if (item.name && item.start && item.stop) { + const start = dayjs.unix(parseInt(item.start) * 60) + const stop = dayjs.unix(parseInt(item.stop) * 60) + + programs.push({ + title: item.name, + description: item.description, + start: start.toString(), + stop: stop.toString() + }) + } + }) + + return programs + } +} diff --git a/sites/telsu.fi/telsu.fi.config.js b/sites/telsu.fi/telsu.fi.config.js index cee4a1fea..4aae84523 100644 --- a/sites/telsu.fi/telsu.fi.config.js +++ b/sites/telsu.fi/telsu.fi.config.js @@ -1,98 +1,98 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'telsu.fi', - days: 2, - url: function ({ date, channel }) { - return `https://www.telsu.fi/${date.format('YYYYMMDD')}/${channel.site_id}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - let stop = parseStop($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - } - prev.stop = start - if (stop.isBefore(prev.stop)) { - stop = stop.add(1, 'd') - } - } - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.telsu.fi/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('.ch').toArray() - return items.map(item => { - const name = $(item).find('a').attr('title') - const site_id = $(item).attr('rel') - - return { - lang: 'fi', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('h1 > b').text().trim() -} - -function parseDescription($item) { - return $item('.t > div').clone().children().remove().end().text().trim() -} - -function parseIcon($item) { - const imgSrc = $item('.t > div > div.ps > a > img').attr('src') - - return imgSrc ? `https://www.telsu.fi${imgSrc}` : null -} - -function parseStart($item, date) { - const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() - const [, HH, mm] = subtitle.match(/(\d{2})\.(\d{2}) - (\d{2})\.(\d{2})$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') -} - -function parseStop($item, date) { - const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() - const [, HH, mm] = subtitle.match(/ - (\d{2})\.(\d{2})$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#res > div.dets').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'telsu.fi', + days: 2, + url: function ({ date, channel }) { + return `https://www.telsu.fi/${date.format('YYYYMMDD')}/${channel.site_id}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + let stop = parseStop($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + } + prev.stop = start + if (stop.isBefore(prev.stop)) { + stop = stop.add(1, 'd') + } + } + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.telsu.fi/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('.ch').toArray() + return items.map(item => { + const name = $(item).find('a').attr('title') + const site_id = $(item).attr('rel') + + return { + lang: 'fi', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('h1 > b').text().trim() +} + +function parseDescription($item) { + return $item('.t > div').clone().children().remove().end().text().trim() +} + +function parseIcon($item) { + const imgSrc = $item('.t > div > div.ps > a > img').attr('src') + + return imgSrc ? `https://www.telsu.fi${imgSrc}` : null +} + +function parseStart($item, date) { + const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() + const [, HH, mm] = subtitle.match(/(\d{2})\.(\d{2}) - (\d{2})\.(\d{2})$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') +} + +function parseStop($item, date) { + const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() + const [, HH, mm] = subtitle.match(/ - (\d{2})\.(\d{2})$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#res > div.dets').toArray() +} diff --git a/sites/telsu.fi/telsu.fi.test.js b/sites/telsu.fi/telsu.fi.test.js index e2e995779..b3f17a9b3 100644 --- a/sites/telsu.fi/telsu.fi.test.js +++ b/sites/telsu.fi/telsu.fi.test.js @@ -1,47 +1,47 @@ -// npm run channels:parse -- --config=./sites/telsu.fi/telsu.fi.config.js --output=./sites/telsu.fi/telsu.fi.channels.xml -// npm run grab -- --site=telsu.fi - -const { parser, url } = require('./telsu.fi.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'yle1', - xmltv_id: 'YleTV1.fi' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://www.telsu.fi/20221029/yle1') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T04:00:00.000Z', - stop: '2022-10-29T04:28:00.000Z', - title: 'Antiikkikaksintaistelu', - description: - 'Kausi 6, osa 5/12. Antiikkikaksintaistelu jatkuu Løkkenissä. Uusi taistelupari Rikke Fog ja Lasse Franck saavat kumpikin 10 000 kruunua ja viisi tuntia aikaa ostaa alueelta hyvää tavaraa halvalla.', - icon: 'https://www.telsu.fi/s/antiikkikaksintaistelu_11713730.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - date - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/telsu.fi/telsu.fi.config.js --output=./sites/telsu.fi/telsu.fi.channels.xml +// npm run grab -- --site=telsu.fi + +const { parser, url } = require('./telsu.fi.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'yle1', + xmltv_id: 'YleTV1.fi' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://www.telsu.fi/20221029/yle1') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T04:00:00.000Z', + stop: '2022-10-29T04:28:00.000Z', + title: 'Antiikkikaksintaistelu', + description: + 'Kausi 6, osa 5/12. Antiikkikaksintaistelu jatkuu Løkkenissä. Uusi taistelupari Rikke Fog ja Lasse Franck saavat kumpikin 10 000 kruunua ja viisi tuntia aikaa ostaa alueelta hyvää tavaraa halvalla.', + icon: 'https://www.telsu.fi/s/antiikkikaksintaistelu_11713730.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tivu.tv/tivu.tv.config.js b/sites/tivu.tv/tivu.tv.config.js index 5e8dcd5fd..8f16d2d4d 100644 --- a/sites/tivu.tv/tivu.tv.config.js +++ b/sites/tivu.tv/tivu.tv.config.js @@ -1,64 +1,64 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tivu.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - const diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') - - return `https://www.tivu.tv/epg_ajax_sat.aspx?d=${diff}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - const [title] = $item('a').html().split('
    ') - - return title -} - -function parseStart($item, date) { - const [, , time] = $item('a').html().split('
    ') - if (!time) return null - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Rome' - }).toUTC() -} - -function parseItems(content, channel) { - if (!content) return [] - const $ = cheerio.load(content) - - return $(`.q[id="${channel.site_id}"] > .p`).toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tivu.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + const diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') + + return `https://www.tivu.tv/epg_ajax_sat.aspx?d=${diff}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + const [title] = $item('a').html().split('
    ') + + return title +} + +function parseStart($item, date) { + const [, , time] = $item('a').html().split('
    ') + if (!time) return null + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Rome' + }).toUTC() +} + +function parseItems(content, channel) { + if (!content) return [] + const $ = cheerio.load(content) + + return $(`.q[id="${channel.site_id}"] > .p`).toArray() +} diff --git a/sites/tivu.tv/tivu.tv.test.js b/sites/tivu.tv/tivu.tv.test.js index e8521a0d8..47ed98e9f 100644 --- a/sites/tivu.tv/tivu.tv.test.js +++ b/sites/tivu.tv/tivu.tv.test.js @@ -1,58 +1,58 @@ -// npm run grab -- --site=tivu.tv - -const { parser, url } = require('./tivu.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const channel = { - site_id: '62', - xmltv_id: 'Rai1HD.it' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=0') -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=1') -}) - -it('can parse response', () => { - const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-03T22:02:00.000Z', - stop: '2022-10-03T22:45:00.000Z', - title: 'Cose Nostre - La figlia del boss' - }) - - expect(results[43]).toMatchObject({ - start: '2022-10-05T04:58:00.000Z', - stop: '2022-10-05T05:28:00.000Z', - title: 'Tgunomattina - in collaborazione con day' - }) -}) - -it('can handle empty guide', () => { - const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tivu.tv + +const { parser, url } = require('./tivu.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const channel = { + site_id: '62', + xmltv_id: 'Rai1HD.it' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=0') +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=1') +}) + +it('can parse response', () => { + const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-03T22:02:00.000Z', + stop: '2022-10-03T22:45:00.000Z', + title: 'Cose Nostre - La figlia del boss' + }) + + expect(results[43]).toMatchObject({ + start: '2022-10-05T04:58:00.000Z', + stop: '2022-10-05T05:28:00.000Z', + title: 'Tgunomattina - in collaborazione con day' + }) +}) + +it('can handle empty guide', () => { + const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/toonamiaftermath.com/toonamiaftermath.com.config.js b/sites/toonamiaftermath.com/toonamiaftermath.com.config.js index effd50292..be0cac53b 100644 --- a/sites/toonamiaftermath.com/toonamiaftermath.com.config.js +++ b/sites/toonamiaftermath.com/toonamiaftermath.com.config.js @@ -1,60 +1,60 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' - -const dayjs = require('dayjs') -const axios = require('axios') - -const API_ENDPOINT = 'https://api.toonamiaftermath.com' - -module.exports = { - site: 'toonamiaftermath.com', - days: 3, - async url({ channel, date }) { - const playlists = await axios - .get( - `${API_ENDPOINT}/playlists?scheduleName=${channel.site_id}&startDate=${date - .add(1, 'd') - .toJSON()}&thisWeek=true&weekStartDay=monday` - ) - .then(r => r.data) - .catch(console.error) - - const playlist = playlists.find(p => date.isSame(p.startDate, 'day')) - - return `${API_ENDPOINT}/playlist?id=${playlist._id}&addInfo=true` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - sub_title: parseEpisode(item), - icon: parseIcon(item), - start: dayjs(item.startDate), - stop: dayjs(item.endDate) - }) - }) - - return programs - } -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !data.playlist) return [] - - return data.playlist.blocks.reduce((acc, curr) => { - acc = acc.concat(curr.mediaList) - - return acc - }, []) -} - -function parseEpisode(item) { - return item && item.info && item.info.episode ? item.info.episode : null -} - -function parseIcon(item) { - return item && item.info && item.info.image ? item.info.image : null -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' + +const dayjs = require('dayjs') +const axios = require('axios') + +const API_ENDPOINT = 'https://api.toonamiaftermath.com' + +module.exports = { + site: 'toonamiaftermath.com', + days: 3, + async url({ channel, date }) { + const playlists = await axios + .get( + `${API_ENDPOINT}/playlists?scheduleName=${channel.site_id}&startDate=${date + .add(1, 'd') + .toJSON()}&thisWeek=true&weekStartDay=monday` + ) + .then(r => r.data) + .catch(console.error) + + const playlist = playlists.find(p => date.isSame(p.startDate, 'day')) + + return `${API_ENDPOINT}/playlist?id=${playlist._id}&addInfo=true` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + sub_title: parseEpisode(item), + icon: parseIcon(item), + start: dayjs(item.startDate), + stop: dayjs(item.endDate) + }) + }) + + return programs + } +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !data.playlist) return [] + + return data.playlist.blocks.reduce((acc, curr) => { + acc = acc.concat(curr.mediaList) + + return acc + }, []) +} + +function parseEpisode(item) { + return item && item.info && item.info.episode ? item.info.episode : null +} + +function parseIcon(item) { + return item && item.info && item.info.image ? item.info.image : null +} diff --git a/sites/toonamiaftermath.com/toonamiaftermath.com.test.js b/sites/toonamiaftermath.com/toonamiaftermath.com.test.js index 23418757a..47f99ef9d 100644 --- a/sites/toonamiaftermath.com/toonamiaftermath.com.test.js +++ b/sites/toonamiaftermath.com/toonamiaftermath.com.test.js @@ -1,63 +1,63 @@ -// npm run grab -- --site=toonamiaftermath.com - -const { parser, url } = require('./toonamiaftermath.com.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const API_ENDPOINT = 'https://api.toonamiaftermath.com' - -const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Toonami Aftermath EST', - xmltv_id: 'ToonamiAftermathEast.us' -} - -it('can generate valid url', async () => { - axios.get.mockImplementation(url => { - if ( - url === - `${API_ENDPOINT}/playlists?scheduleName=Toonami Aftermath EST&startDate=2022-11-30T00:00:00.000Z&thisWeek=true&weekStartDay=monday` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/playlists.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const result = await url({ channel, date }) - - expect(result).toBe(`${API_ENDPOINT}/playlist?id=635fbd8117f6824d953a216e&addInfo=true`) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(62) - expect(results[0]).toMatchObject({ - start: '2022-11-29T17:00:30.231Z', - stop: '2022-11-29T17:20:54.031Z', - title: 'X-Men', - sub_title: 'Reunion (Part 1)', - icon: 'https://i.imgur.com/ZSZ0x1m.gif' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=toonamiaftermath.com + +const { parser, url } = require('./toonamiaftermath.com.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const API_ENDPOINT = 'https://api.toonamiaftermath.com' + +const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Toonami Aftermath EST', + xmltv_id: 'ToonamiAftermathEast.us' +} + +it('can generate valid url', async () => { + axios.get.mockImplementation(url => { + if ( + url === + `${API_ENDPOINT}/playlists?scheduleName=Toonami Aftermath EST&startDate=2022-11-30T00:00:00.000Z&thisWeek=true&weekStartDay=monday` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/playlists.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const result = await url({ channel, date }) + + expect(result).toBe(`${API_ENDPOINT}/playlist?id=635fbd8117f6824d953a216e&addInfo=true`) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(62) + expect(results[0]).toMatchObject({ + start: '2022-11-29T17:00:30.231Z', + stop: '2022-11-29T17:20:54.031Z', + title: 'X-Men', + sub_title: 'Reunion (Part 1)', + icon: 'https://i.imgur.com/ZSZ0x1m.gif' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/transvision.co.id/transvision.co.id.config.js b/sites/transvision.co.id/transvision.co.id.config.js index 50f9d9adc..ae1ecc217 100644 --- a/sites/transvision.co.id/transvision.co.id.config.js +++ b/sites/transvision.co.id/transvision.co.id.config.js @@ -1,93 +1,93 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'transvision.co.id', - days: 2, - url: 'https://www.transvision.co.id/jadwalacara/epg', - request: { - method: 'POST', - data: function ({ channel, date }) { - const formData = new URLSearchParams() - formData.append('ValidateEPG[channel_name]', channel.site_id) - formData.append('ValidateEPG[tanggal]', date.format('YYYY-MM-DD')) - formData.append('ValidateEPG[sinopsis]', '') - formData.append('yt0', 'PROSES') - - return formData - }, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - for (const item of items) { - const prev = programs[programs.length - 1] - const start = parseStart(item, date) - if (prev) prev.stop = start - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://www.transvision.co.id/jadwalacara/epg') - .then(response => response.data) - .catch(console.log) - - const $ = cheerio.load(data) - const items = $('#ValidateEPG_channel_name option').toArray() - const channels = items.map(item => { - const $item = cheerio.load(item) - - return { - lang: 'id', - site_id: $item('*').attr('value'), - name: $item('*').text() - } - }) - - return channels - } -} - -function parseDescription(item) { - return cheerio.load(item)('td:last-child').text() -} - -function parseStart(item, date) { - const $ = cheerio.load(item) - let time = $('th').text() - time = `${date.format('DD/MM/YYYY')} ${time}` - - return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('td:first-of-type').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('table tbody tr').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'transvision.co.id', + days: 2, + url: 'https://www.transvision.co.id/jadwalacara/epg', + request: { + method: 'POST', + data: function ({ channel, date }) { + const formData = new URLSearchParams() + formData.append('ValidateEPG[channel_name]', channel.site_id) + formData.append('ValidateEPG[tanggal]', date.format('YYYY-MM-DD')) + formData.append('ValidateEPG[sinopsis]', '') + formData.append('yt0', 'PROSES') + + return formData + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + for (const item of items) { + const prev = programs[programs.length - 1] + const start = parseStart(item, date) + if (prev) prev.stop = start + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://www.transvision.co.id/jadwalacara/epg') + .then(response => response.data) + .catch(console.log) + + const $ = cheerio.load(data) + const items = $('#ValidateEPG_channel_name option').toArray() + const channels = items.map(item => { + const $item = cheerio.load(item) + + return { + lang: 'id', + site_id: $item('*').attr('value'), + name: $item('*').text() + } + }) + + return channels + } +} + +function parseDescription(item) { + return cheerio.load(item)('td:last-child').text() +} + +function parseStart(item, date) { + const $ = cheerio.load(item) + let time = $('th').text() + time = `${date.format('DD/MM/YYYY')} ${time}` + + return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('td:first-of-type').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('table tbody tr').toArray() +} diff --git a/sites/transvision.co.id/transvision.co.id.test.js b/sites/transvision.co.id/transvision.co.id.test.js index 6fc3c5d30..b04f97f7a 100644 --- a/sites/transvision.co.id/transvision.co.id.test.js +++ b/sites/transvision.co.id/transvision.co.id.test.js @@ -1,82 +1,82 @@ -// npm run grab -- --site=transvision.co.id - -const { parser, url, request } = require('./transvision.co.id.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'TRIS', - xmltv_id: 'nsert.id' -} -const content = - '
    00:00:00 Insert TodayInsert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.
    01:00:00 BrownisBrownis atau obrolan manis merupakan program talkshow segar yang dipandu oleh Ruben Onsu bersama Ivan Gunawan.
    01:30:00 Warga +62Warga +62 menghadirkan trend penyebaran video/momen lucu yang juga dikenal sebagai video lucu Indonesia yang tersebar di media sosial.
    23:00:00 InsertInsert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.
    ' - -it('can generate valid url', () => { - expect(url).toBe('https://www.transvision.co.id/jadwalacara/epg') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.get('ValidateEPG[channel_name]')).toBe('TRIS') - expect(result.get('ValidateEPG[tanggal]')).toBe('2022-03-10') - expect(result.get('ValidateEPG[sinopsis]')).toBe('') - expect(result.get('yt0')).toBe('PROSES') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Insert Today', - description: - 'Insert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.', - start: '2022-03-09T17:00:00.000Z', - stop: '2022-03-09T18:00:00.000Z' - }, - { - title: 'Brownis', - description: - 'Brownis atau obrolan manis merupakan program talkshow segar yang dipandu oleh Ruben Onsu bersama Ivan Gunawan.', - start: '2022-03-09T18:00:00.000Z', - stop: '2022-03-09T18:30:00.000Z' - }, - { - title: 'Warga +62', - description: - 'Warga +62 menghadirkan trend penyebaran video/momen lucu yang juga dikenal sebagai video lucu Indonesia yang tersebar di media sosial.', - start: '2022-03-09T18:30:00.000Z', - stop: '2022-03-10T16:00:00.000Z' - }, - { - title: 'Insert', - description: - 'Insert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.', - start: '2022-03-10T16:00:00.000Z', - stop: '2022-03-10T16:30:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=transvision.co.id + +const { parser, url, request } = require('./transvision.co.id.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'TRIS', + xmltv_id: 'nsert.id' +} +const content = + '
    00:00:00 Insert TodayInsert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.
    01:00:00 BrownisBrownis atau obrolan manis merupakan program talkshow segar yang dipandu oleh Ruben Onsu bersama Ivan Gunawan.
    01:30:00 Warga +62Warga +62 menghadirkan trend penyebaran video/momen lucu yang juga dikenal sebagai video lucu Indonesia yang tersebar di media sosial.
    23:00:00 InsertInsert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.
    ' + +it('can generate valid url', () => { + expect(url).toBe('https://www.transvision.co.id/jadwalacara/epg') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.get('ValidateEPG[channel_name]')).toBe('TRIS') + expect(result.get('ValidateEPG[tanggal]')).toBe('2022-03-10') + expect(result.get('ValidateEPG[sinopsis]')).toBe('') + expect(result.get('yt0')).toBe('PROSES') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Insert Today', + description: + 'Insert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.', + start: '2022-03-09T17:00:00.000Z', + stop: '2022-03-09T18:00:00.000Z' + }, + { + title: 'Brownis', + description: + 'Brownis atau obrolan manis merupakan program talkshow segar yang dipandu oleh Ruben Onsu bersama Ivan Gunawan.', + start: '2022-03-09T18:00:00.000Z', + stop: '2022-03-09T18:30:00.000Z' + }, + { + title: 'Warga +62', + description: + 'Warga +62 menghadirkan trend penyebaran video/momen lucu yang juga dikenal sebagai video lucu Indonesia yang tersebar di media sosial.', + start: '2022-03-09T18:30:00.000Z', + stop: '2022-03-10T16:00:00.000Z' + }, + { + title: 'Insert', + description: + 'Insert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.', + start: '2022-03-10T16:00:00.000Z', + stop: '2022-03-10T16:30:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js b/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js index 3806f3c03..960f5ce8c 100644 --- a/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js +++ b/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js @@ -1,61 +1,61 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'turksatkablo.com.tr', - days: 2, - url: function ({ date }) { - return `https://www.turksatkablo.com.tr/userUpload/EPG/y.json?_=${date.valueOf()}` - }, - request: { - timeout: 60000 - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop(item, date) - if (prev && stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - programs.push({ - title: item.b, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.c}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() -} - -function parseStop(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.d}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() -} - -function parseItems(content, channel) { - let parsed - try { - parsed = JSON.parse(content) - } catch (error) { - return [] - } - if (!parsed || !parsed.k) return [] - const data = parsed.k.find(c => c.x == channel.site_id) - - return data ? data.p : [] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'turksatkablo.com.tr', + days: 2, + url: function ({ date }) { + return `https://www.turksatkablo.com.tr/userUpload/EPG/y.json?_=${date.valueOf()}` + }, + request: { + timeout: 60000 + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop(item, date) + if (prev && stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + programs.push({ + title: item.b, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.c}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() +} + +function parseStop(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.d}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() +} + +function parseItems(content, channel) { + let parsed + try { + parsed = JSON.parse(content) + } catch (error) { + return [] + } + if (!parsed || !parsed.k) return [] + const data = parsed.k.find(c => c.x == channel.site_id) + + return data ? data.p : [] +} diff --git a/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js b/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js index d8faf0281..f33a264f7 100644 --- a/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js +++ b/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=turksatkablo.com.tr - -const { parser, url } = require('./turksatkablo.com.tr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-25', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '283', xmltv_id: 'SinemaTV.tr', display_name: 'Sinema TV' } -const content = - '{"k":[{"x":283,"i":2,"n":"Sinema TV","p":[{"a":"196432597608","b":"Ölüm Ormanı","c":"01:15","d":"03:00"},{"a":"196432597628","b":"Kızım","c":"15:00","d":"17:00"},{"a": "196441294843","b":"Kaçakçı","c":"23:45","d":"03:45"}]}]}' - -it('can generate valid url', () => { - const result = url({ date }) - expect(result).toBe('https://www.turksatkablo.com.tr/userUpload/EPG/y.json?_=1635120000000') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-10-24T22:15:00.000Z', - stop: '2021-10-25T00:00:00.000Z', - title: 'Ölüm Ormanı' - }, - { - start: '2021-10-25T12:00:00.000Z', - stop: '2021-10-25T14:00:00.000Z', - title: 'Kızım' - }, - { - start: '2021-10-25T20:45:00.000Z', - stop: '2021-10-26T00:45:00.000Z', - title: 'Kaçakçı' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=turksatkablo.com.tr + +const { parser, url } = require('./turksatkablo.com.tr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-25', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '283', xmltv_id: 'SinemaTV.tr', display_name: 'Sinema TV' } +const content = + '{"k":[{"x":283,"i":2,"n":"Sinema TV","p":[{"a":"196432597608","b":"Ölüm Ormanı","c":"01:15","d":"03:00"},{"a":"196432597628","b":"Kızım","c":"15:00","d":"17:00"},{"a": "196441294843","b":"Kaçakçı","c":"23:45","d":"03:45"}]}]}' + +it('can generate valid url', () => { + const result = url({ date }) + expect(result).toBe('https://www.turksatkablo.com.tr/userUpload/EPG/y.json?_=1635120000000') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-10-24T22:15:00.000Z', + stop: '2021-10-25T00:00:00.000Z', + title: 'Ölüm Ormanı' + }, + { + start: '2021-10-25T12:00:00.000Z', + stop: '2021-10-25T14:00:00.000Z', + title: 'Kızım' + }, + { + start: '2021-10-25T20:45:00.000Z', + stop: '2021-10-26T00:45:00.000Z', + title: 'Kaçakçı' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.blue.ch/tv.blue.ch.config.js b/sites/tv.blue.ch/tv.blue.ch.config.js index 8c130b493..c1c72a90e 100644 --- a/sites/tv.blue.ch/tv.blue.ch.config.js +++ b/sites/tv.blue.ch/tv.blue.ch.config.js @@ -1,85 +1,85 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'tv.blue.ch', - days: 2, - url: function ({ channel, date }) { - return `https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=${ - channel.site_id - };start=${date.format('YYYYMMDDHHss')};end=${date - .add(1, 'd') - .format('YYYYMMDDHHss')};level=normal)` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const title = parseTitle(item) - if (title === 'Sendepause') return - programs.push({ - title, - description: parseDescription(item), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://services.sg101.prd.sctv.ch/portfolio/tv/channels') - .then(r => r.data) - .catch(console.log) - - return items.map(item => { - return { - lang: item.Languages[0] || 'de', - site_id: item.Identifier, - name: item.Title - } - }) - } -} - -function parseTitle(item) { - return item.Content.Description.Title -} - -function parseDescription(item) { - return item.Content.Description.Summary -} - -function parseIcon(item) { - const image = item.Content.Nodes ? item.Content.Nodes.Items.find(i => i.Kind === 'Image') : null - const path = image ? image.ContentPath : null - - return path ? `https://services.sg101.prd.sctv.ch/content/images${path}_w1920.webp` : null -} - -function parseStart(item) { - const available = item.Availabilities.length ? item.Availabilities[0] : null - - return dayjs(available.AvailabilityStart) -} - -function parseStop(item) { - const available = item.Availabilities.length ? item.Availabilities[0] : null - - return dayjs(available.AvailabilityEnd) -} - -function parseItems(content) { - const data = JSON.parse(content) - const nodes = data.Nodes.Items.filter(i => i.Kind === 'Channel') - if (!nodes.length) return [] - - return nodes[0].Content.Nodes && Array.isArray(nodes[0].Content.Nodes.Items) - ? nodes[0].Content.Nodes.Items.filter(i => i.Kind === 'Broadcast') - : [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'tv.blue.ch', + days: 2, + url: function ({ channel, date }) { + return `https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=${ + channel.site_id + };start=${date.format('YYYYMMDDHHss')};end=${date + .add(1, 'd') + .format('YYYYMMDDHHss')};level=normal)` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const title = parseTitle(item) + if (title === 'Sendepause') return + programs.push({ + title, + description: parseDescription(item), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://services.sg101.prd.sctv.ch/portfolio/tv/channels') + .then(r => r.data) + .catch(console.log) + + return items.map(item => { + return { + lang: item.Languages[0] || 'de', + site_id: item.Identifier, + name: item.Title + } + }) + } +} + +function parseTitle(item) { + return item.Content.Description.Title +} + +function parseDescription(item) { + return item.Content.Description.Summary +} + +function parseIcon(item) { + const image = item.Content.Nodes ? item.Content.Nodes.Items.find(i => i.Kind === 'Image') : null + const path = image ? image.ContentPath : null + + return path ? `https://services.sg101.prd.sctv.ch/content/images${path}_w1920.webp` : null +} + +function parseStart(item) { + const available = item.Availabilities.length ? item.Availabilities[0] : null + + return dayjs(available.AvailabilityStart) +} + +function parseStop(item) { + const available = item.Availabilities.length ? item.Availabilities[0] : null + + return dayjs(available.AvailabilityEnd) +} + +function parseItems(content) { + const data = JSON.parse(content) + const nodes = data.Nodes.Items.filter(i => i.Kind === 'Channel') + if (!nodes.length) return [] + + return nodes[0].Content.Nodes && Array.isArray(nodes[0].Content.Nodes.Items) + ? nodes[0].Content.Nodes.Items.filter(i => i.Kind === 'Broadcast') + : [] +} diff --git a/sites/tv.blue.ch/tv.blue.ch.test.js b/sites/tv.blue.ch/tv.blue.ch.test.js index 73391dde6..873ae1824 100644 --- a/sites/tv.blue.ch/tv.blue.ch.test.js +++ b/sites/tv.blue.ch/tv.blue.ch.test.js @@ -1,76 +1,76 @@ -// node ./scripts/commands/parse-channels.js --config=./sites/tv.blue.ch/tv.blue.ch.config.js --output=./sites/tv.blue.ch/tv.blue.ch.channels.xml -// npm run grab -- --site=tv.blue.ch - -const { parser, url } = require('./tv.blue.ch.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1221', - xmltv_id: 'BlueZoomD.ch' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)' - ) -}) - -it('can parse response', () => { - const content = - '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45","Kind":"Broadcast","Channel":"1221","Content":{"Description":{"Title":"Weekend on the Rocks","Summary":" - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.","ShortSummary":"","Country":"CH","ReleaseDate":"2021-01-01T00:00:00Z","Source":"13","Language":"de","Duration":"00:30:00"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45_landscape","Kind":"Image","Role":"Landscape","ContentPath":"/tv/broadcast/1221/t1221ddc59247d45_landscape","Version":{"Date":"2022-01-04T08:55:22.567Z"}}]},"TechnicalAttributes":{"Stereo":true}},"Version":{"Hash":"60d3"},"Availabilities":[{"AvailabilityStart":"2022-01-16T23:30:00Z","AvailabilityEnd":"2022-01-17T00:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"2b0898c7-3920-3200-7048-4ea5d9138921"},{"Domain":"TV","Kind":"Reference","Role":"OriginalAirSeries","TargetIdentifier":"false"},{"Domain":"TV","Kind":"Reference","Role":"ExternalBroadcastIdentifier","TargetIdentifier":"167324536-11"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p12211351631155","Title":"Original"}]}]}}}]}}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-16T23:30:00.000Z', - stop: '2022-01-17T00:00:00.000Z', - title: 'Weekend on the Rocks', - description: - ' - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.', - icon: 'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp' - } - ]) -}) - -it('can parse response without image', () => { - const content = - '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t10014a78a8b0668","Kind":"Broadcast","Channel":"1001","Content":{"Description":{"Title":"Lorem ipsum","Language":"fr","Duration":"00:01:00"}},"Version":{"Hash":"440e"},"Availabilities":[{"AvailabilityStart":"2022-01-17T04:59:00Z","AvailabilityEnd":"2022-01-17T05:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"3553a4f2-ff63-5200-7048-d8d59d805f81"},{"Domain":"TV","Kind":"Reference","Role":"Dummy","TargetIdentifier":"True"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p1"}]}]}}}]}}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-17T04:59:00.000Z', - stop: '2022-01-17T05:00:00.000Z', - title: 'Lorem ipsum' - } - ]) -}) - -it('can handle wrong site id', () => { - const result = parser({ - content: - '{"Status":{"Version":"7","Status":"OK","ProcessingTime":"00:00:00.0160674","ExecutionTime":"2022-01-17T13:47:30.584Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=12210;start=202201170000;end=202201180000;level=normal)","Identifiers":["12210"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117114748","DbCreationTime":"2022-01-17T11:49:14.608Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Items":[]}}' - }) - expect(result).toMatchObject([]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '{"Status":{"Version":"7","Status":"OK","ExecutionTime":"2022-01-17T15:30:37.97Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=1884;start=202201170000;end=202201180000;level=normal)","Identifiers":["1884"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117144354","DbCreationTime":"2022-01-17T14:45:11.84Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1884","Kind":"Channel","Content":{"Description":{"Title":"Fisu.tv 1","Language":"en"}}}]}}' - }) - expect(result).toMatchObject([]) -}) +// node ./scripts/commands/parse-channels.js --config=./sites/tv.blue.ch/tv.blue.ch.config.js --output=./sites/tv.blue.ch/tv.blue.ch.channels.xml +// npm run grab -- --site=tv.blue.ch + +const { parser, url } = require('./tv.blue.ch.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1221', + xmltv_id: 'BlueZoomD.ch' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)' + ) +}) + +it('can parse response', () => { + const content = + '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45","Kind":"Broadcast","Channel":"1221","Content":{"Description":{"Title":"Weekend on the Rocks","Summary":" - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.","ShortSummary":"","Country":"CH","ReleaseDate":"2021-01-01T00:00:00Z","Source":"13","Language":"de","Duration":"00:30:00"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45_landscape","Kind":"Image","Role":"Landscape","ContentPath":"/tv/broadcast/1221/t1221ddc59247d45_landscape","Version":{"Date":"2022-01-04T08:55:22.567Z"}}]},"TechnicalAttributes":{"Stereo":true}},"Version":{"Hash":"60d3"},"Availabilities":[{"AvailabilityStart":"2022-01-16T23:30:00Z","AvailabilityEnd":"2022-01-17T00:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"2b0898c7-3920-3200-7048-4ea5d9138921"},{"Domain":"TV","Kind":"Reference","Role":"OriginalAirSeries","TargetIdentifier":"false"},{"Domain":"TV","Kind":"Reference","Role":"ExternalBroadcastIdentifier","TargetIdentifier":"167324536-11"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p12211351631155","Title":"Original"}]}]}}}]}}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-16T23:30:00.000Z', + stop: '2022-01-17T00:00:00.000Z', + title: 'Weekend on the Rocks', + description: + ' - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.', + icon: 'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp' + } + ]) +}) + +it('can parse response without image', () => { + const content = + '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t10014a78a8b0668","Kind":"Broadcast","Channel":"1001","Content":{"Description":{"Title":"Lorem ipsum","Language":"fr","Duration":"00:01:00"}},"Version":{"Hash":"440e"},"Availabilities":[{"AvailabilityStart":"2022-01-17T04:59:00Z","AvailabilityEnd":"2022-01-17T05:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"3553a4f2-ff63-5200-7048-d8d59d805f81"},{"Domain":"TV","Kind":"Reference","Role":"Dummy","TargetIdentifier":"True"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p1"}]}]}}}]}}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-17T04:59:00.000Z', + stop: '2022-01-17T05:00:00.000Z', + title: 'Lorem ipsum' + } + ]) +}) + +it('can handle wrong site id', () => { + const result = parser({ + content: + '{"Status":{"Version":"7","Status":"OK","ProcessingTime":"00:00:00.0160674","ExecutionTime":"2022-01-17T13:47:30.584Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=12210;start=202201170000;end=202201180000;level=normal)","Identifiers":["12210"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117114748","DbCreationTime":"2022-01-17T11:49:14.608Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Items":[]}}' + }) + expect(result).toMatchObject([]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '{"Status":{"Version":"7","Status":"OK","ExecutionTime":"2022-01-17T15:30:37.97Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=1884;start=202201170000;end=202201180000;level=normal)","Identifiers":["1884"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117144354","DbCreationTime":"2022-01-17T14:45:11.84Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1884","Kind":"Channel","Content":{"Description":{"Title":"Fisu.tv 1","Language":"en"}}}]}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.cctv.com/tv.cctv.com.config.js b/sites/tv.cctv.com/tv.cctv.com.config.js index 8f2aa3b40..ad8031615 100644 --- a/sites/tv.cctv.com/tv.cctv.com.config.js +++ b/sites/tv.cctv.com/tv.cctv.com.config.js @@ -1,41 +1,41 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.cctv.com', - days: 2, - url({ channel, date }) { - return `https://api.cntv.cn/epg/getEpgInfoByChannelNew?serviceId=tvcctv&c=${ - channel.site_id - }&d=${date.format('YYYYMMDD')}` - }, - parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const title = item.title - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title, - start, - stop - }) - }) - - return programs - } -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - - return data.data[channel.site_id].list || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.cctv.com', + days: 2, + url({ channel, date }) { + return `https://api.cntv.cn/epg/getEpgInfoByChannelNew?serviceId=tvcctv&c=${ + channel.site_id + }&d=${date.format('YYYYMMDD')}` + }, + parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const title = item.title + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title, + start, + stop + }) + }) + + return programs + } +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + + return data.data[channel.site_id].list || [] +} diff --git a/sites/tv.dir.bg/tv.dir.bg.config.js b/sites/tv.dir.bg/tv.dir.bg.config.js index e43f75638..c1a15fbff 100644 --- a/sites/tv.dir.bg/tv.dir.bg.config.js +++ b/sites/tv.dir.bg/tv.dir.bg.config.js @@ -1,88 +1,88 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tv.dir.bg', - days: 2, - url({ channel, date }) { - return `https://tv.dir.bg/tv_channel.php?id=${channel.site_id}&dd=${date.format('DD.MM')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const requests = [ - axios.get('https://tv.dir.bg/programata.php?t=0'), - axios.get('https://tv.dir.bg/programata.php?t=1') - ] - - const items = await Promise.all(requests) - .then(r => { - return r - .map(i => { - const html = i.data - const $ = cheerio.load(html) - return $('#programa-left > div > div > div > a').toArray() - }) - .reduce((acc, curr) => { - acc = acc.concat(curr) - return acc - }, []) - }) - .catch(console.log) - - const $ = cheerio.load('') - return items.map(item => { - const $item = $(item) - return { - lang: 'bg', - site_id: $item.attr('href').replace('tv_channel.php?id=', ''), - name: $item.find('div.thumbnail > img').attr('alt') - } - }) - } -} - -function parseStart($item, date) { - const time = $item('i').text() - if (!time) return null - const dateString = `${date.format('MM/DD/YYYY')} ${time}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Sofia' }).toUTC() -} - -function parseTitle($item) { - return $item - .text() - .replace(/^\d{2}.\d{2}/, '') - .trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#events > li').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tv.dir.bg', + days: 2, + url({ channel, date }) { + return `https://tv.dir.bg/tv_channel.php?id=${channel.site_id}&dd=${date.format('DD.MM')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const requests = [ + axios.get('https://tv.dir.bg/programata.php?t=0'), + axios.get('https://tv.dir.bg/programata.php?t=1') + ] + + const items = await Promise.all(requests) + .then(r => { + return r + .map(i => { + const html = i.data + const $ = cheerio.load(html) + return $('#programa-left > div > div > div > a').toArray() + }) + .reduce((acc, curr) => { + acc = acc.concat(curr) + return acc + }, []) + }) + .catch(console.log) + + const $ = cheerio.load('') + return items.map(item => { + const $item = $(item) + return { + lang: 'bg', + site_id: $item.attr('href').replace('tv_channel.php?id=', ''), + name: $item.find('div.thumbnail > img').attr('alt') + } + }) + } +} + +function parseStart($item, date) { + const time = $item('i').text() + if (!time) return null + const dateString = `${date.format('MM/DD/YYYY')} ${time}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Sofia' }).toUTC() +} + +function parseTitle($item) { + return $item + .text() + .replace(/^\d{2}.\d{2}/, '') + .trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#events > li').toArray() +} diff --git a/sites/tv.dir.bg/tv.dir.bg.test.js b/sites/tv.dir.bg/tv.dir.bg.test.js index 4b0074e84..0293438e3 100644 --- a/sites/tv.dir.bg/tv.dir.bg.test.js +++ b/sites/tv.dir.bg/tv.dir.bg.test.js @@ -1,57 +1,57 @@ -// node ./scripts/commands/parse-channels.js --config=./sites/tv.dir.bg/tv.dir.bg.config.js --output=./sites/tv.dir.bg/tv.dir.bg.channels.xml -// npm run grab -- --site=tv.dir.bg - -const { parser, url } = require('./tv.dir.bg.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-01-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '12', - xmltv_id: 'BTV.bg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv.dir.bg/tv_channel.php?id=12&dd=20.01') -}) - -it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-20T04:00:00.000Z', - stop: '2022-01-20T13:00:00.000Z', - title: '„Тази сутрин” - информационно предаване с водещи Златимир Йочеви Биляна Гавазова' - }, - { - start: '2022-01-20T13:00:00.000Z', - stop: '2022-01-21T03:30:00.000Z', - title: '„Доктор Чудо” - сериал, еп.71' - }, - { - start: '2022-01-21T03:30:00.000Z', - stop: '2022-01-21T04:00:00.000Z', - title: '„Лице в лице” /п./' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '
      ' - }) - expect(result).toMatchObject([]) -}) +// node ./scripts/commands/parse-channels.js --config=./sites/tv.dir.bg/tv.dir.bg.config.js --output=./sites/tv.dir.bg/tv.dir.bg.channels.xml +// npm run grab -- --site=tv.dir.bg + +const { parser, url } = require('./tv.dir.bg.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '12', + xmltv_id: 'BTV.bg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv.dir.bg/tv_channel.php?id=12&dd=20.01') +}) + +it('can parse response', () => { + const content = + '' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-20T04:00:00.000Z', + stop: '2022-01-20T13:00:00.000Z', + title: '„Тази сутрин” - информационно предаване с водещи Златимир Йочеви Биляна Гавазова' + }, + { + start: '2022-01-20T13:00:00.000Z', + stop: '2022-01-21T03:30:00.000Z', + title: '„Доктор Чудо” - сериал, еп.71' + }, + { + start: '2022-01-21T03:30:00.000Z', + stop: '2022-01-21T04:00:00.000Z', + title: '„Лице в лице” /п./' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '
        ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.lv/tv.lv.config.js b/sites/tv.lv/tv.lv.config.js index 985c8270d..fa6549e54 100644 --- a/sites/tv.lv/tv.lv.config.js +++ b/sites/tv.lv/tv.lv.config.js @@ -1,43 +1,43 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.lv', - days: 2, - url: function ({ date, channel }) { - return `https://www.tv.lv/programme/listing/none/${date.format( - 'DD-MM-YYYY' - )}?filter=channel&subslug=${channel.site_id}` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title: item.title, - description: item.description_long, - category: item.categorystring, - icon: item.image, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - return item.start_unix ? dayjs.unix(item.start_unix) : null -} - -function parseStop(item) { - return item.stop_unix ? dayjs.unix(item.stop_unix) : null -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.schedule.programme || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.lv', + days: 2, + url: function ({ date, channel }) { + return `https://www.tv.lv/programme/listing/none/${date.format( + 'DD-MM-YYYY' + )}?filter=channel&subslug=${channel.site_id}` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title: item.title, + description: item.description_long, + category: item.categorystring, + icon: item.image, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + return item.start_unix ? dayjs.unix(item.start_unix) : null +} + +function parseStop(item) { + return item.stop_unix ? dayjs.unix(item.stop_unix) : null +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.schedule.programme || [] +} diff --git a/sites/tv.mail.ru/tv.mail.ru.config.js b/sites/tv.mail.ru/tv.mail.ru.config.js index 2082a21e6..322114046 100644 --- a/sites/tv.mail.ru/tv.mail.ru.config.js +++ b/sites/tv.mail.ru/tv.mail.ru.config.js @@ -1,76 +1,76 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'tv.mail.ru', - days: 2, - delay: 1000, - url({ channel, date }) { - return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: item.name, - category: parseCategory(item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.start}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC() -} - -function parseCategory(item) { - const categories = { - 1: 'Фильм', - 2: 'Сериал', - 6: 'Документальное', - 7: 'Телемагазин', - 8: 'Позновательное', - 10: 'Другое', - 14: 'ТВ-шоу', - 16: 'Досуг,Хобби', - 17: 'Ток-шоу', - 18: 'Юмористическое', - 23: 'Музыка', - 24: 'Развлекательное', - 25: 'Игровое', - 26: 'Новости' - } - - return categories[item.category_id] - ? { - lang: 'ru', - value: categories[item.category_id] - } - : null -} - -function parseItems(content) { - const json = JSON.parse(content) - if (!Array.isArray(json.schedule) || !json.schedule[0]) return [] - const event = json.schedule[0].event || [] - - return [...event.past, ...event.current] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'tv.mail.ru', + days: 2, + delay: 1000, + url({ channel, date }) { + return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: item.name, + category: parseCategory(item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.start}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC() +} + +function parseCategory(item) { + const categories = { + 1: 'Фильм', + 2: 'Сериал', + 6: 'Документальное', + 7: 'Телемагазин', + 8: 'Позновательное', + 10: 'Другое', + 14: 'ТВ-шоу', + 16: 'Досуг,Хобби', + 17: 'Ток-шоу', + 18: 'Юмористическое', + 23: 'Музыка', + 24: 'Развлекательное', + 25: 'Игровое', + 26: 'Новости' + } + + return categories[item.category_id] + ? { + lang: 'ru', + value: categories[item.category_id] + } + : null +} + +function parseItems(content) { + const json = JSON.parse(content) + if (!Array.isArray(json.schedule) || !json.schedule[0]) return [] + const event = json.schedule[0].event || [] + + return [...event.past, ...event.current] +} diff --git a/sites/tv.mail.ru/tv.mail.ru.test.js b/sites/tv.mail.ru/tv.mail.ru.test.js index a77b43232..f8797a21f 100644 --- a/sites/tv.mail.ru/tv.mail.ru.test.js +++ b/sites/tv.mail.ru/tv.mail.ru.test.js @@ -1,79 +1,79 @@ -// npm run grab -- --site=tv.mail.ru - -const { parser, url } = require('./tv.mail.ru.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2785', - xmltv_id: '21TV.am' -} -const content = - '{"status":"OK","schedule":[{"channel":{"name":"21TV","pic_url":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png","pic_url_128":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACwjJ45j9sTP8fcjPJnJ4xk5e_ILr5iXwjLMhWhzlVnIJkrtT42vEp9walcgpXRKDq9KFoliEPR0xI-LEh96C_izY.png","pic_url_64":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/dpr:200/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png"},"event":{"current":[{"channel_id":"2785","name":"Պրոֆեսիոնալները","category_id":8,"episode_title":"","url":"/moskva/channel/2785/173593246/","id":"173593246","start":"02:40","episode_num":0},{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593142/","id":"173593142","start":"03:25","episode_num":0}],"past":[{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593328/","id":"173593328","start":"23:35","episode_num":0},{"channel_id":"2785","video":{"currency":"RUB","price_min":"249.00","price_txt":"249 р."},"name":"Վերջին թագավորությունը","category_id":2,"episode_title":"","url":"/moskva/channel/2785/173593318/","id":"173593318","start":"01:40","our_event_id":"890224","episode_num":0}]}}]}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=2785&date=2021-11-24' - ) -}) - -it('can parse response', () => { - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T20:35:00.000Z', - stop: '2021-11-24T22:40:00.000Z', - title: 'Նոնստոպ․ Տեսահոլովակներ', - category: { - lang: 'ru', - value: 'Музыка' - } - }, - { - start: '2021-11-24T22:40:00.000Z', - stop: '2021-11-24T23:40:00.000Z', - title: 'Վերջին թագավորությունը', - category: { - lang: 'ru', - value: 'Сериал' - } - }, - { - start: '2021-11-24T23:40:00.000Z', - stop: '2021-11-25T00:25:00.000Z', - title: 'Պրոֆեսիոնալները', - category: { - lang: 'ru', - value: 'Позновательное' - } - }, - { - start: '2021-11-25T00:25:00.000Z', - stop: '2021-11-25T01:25:00.000Z', - title: 'Նոնստոպ․ Տեսահոլովակներ', - category: { - lang: 'ru', - value: 'Музыка' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"status":"OK","current_ts":1637788593,"form":{"values":[]},"current_offset":10800,"schedule":[{"channel":null,"event":{"current":[],"past":[]}}]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tv.mail.ru + +const { parser, url } = require('./tv.mail.ru.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2785', + xmltv_id: '21TV.am' +} +const content = + '{"status":"OK","schedule":[{"channel":{"name":"21TV","pic_url":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png","pic_url_128":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACwjJ45j9sTP8fcjPJnJ4xk5e_ILr5iXwjLMhWhzlVnIJkrtT42vEp9walcgpXRKDq9KFoliEPR0xI-LEh96C_izY.png","pic_url_64":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/dpr:200/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png"},"event":{"current":[{"channel_id":"2785","name":"Պրոֆեսիոնալները","category_id":8,"episode_title":"","url":"/moskva/channel/2785/173593246/","id":"173593246","start":"02:40","episode_num":0},{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593142/","id":"173593142","start":"03:25","episode_num":0}],"past":[{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593328/","id":"173593328","start":"23:35","episode_num":0},{"channel_id":"2785","video":{"currency":"RUB","price_min":"249.00","price_txt":"249 р."},"name":"Վերջին թագավորությունը","category_id":2,"episode_title":"","url":"/moskva/channel/2785/173593318/","id":"173593318","start":"01:40","our_event_id":"890224","episode_num":0}]}}]}' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=2785&date=2021-11-24' + ) +}) + +it('can parse response', () => { + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T20:35:00.000Z', + stop: '2021-11-24T22:40:00.000Z', + title: 'Նոնստոպ․ Տեսահոլովակներ', + category: { + lang: 'ru', + value: 'Музыка' + } + }, + { + start: '2021-11-24T22:40:00.000Z', + stop: '2021-11-24T23:40:00.000Z', + title: 'Վերջին թագավորությունը', + category: { + lang: 'ru', + value: 'Сериал' + } + }, + { + start: '2021-11-24T23:40:00.000Z', + stop: '2021-11-25T00:25:00.000Z', + title: 'Պրոֆեսիոնալները', + category: { + lang: 'ru', + value: 'Позновательное' + } + }, + { + start: '2021-11-25T00:25:00.000Z', + stop: '2021-11-25T01:25:00.000Z', + title: 'Նոնստոպ․ Տեսահոլովակներ', + category: { + lang: 'ru', + value: 'Музыка' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"status":"OK","current_ts":1637788593,"form":{"values":[]},"current_offset":10800,"schedule":[{"channel":null,"event":{"current":[],"past":[]}}]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js b/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js index 0595bf455..1d0a83ceb 100644 --- a/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js +++ b/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js @@ -1,57 +1,57 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -module.exports = { - site: 'tv.movistar.com.pe', - days: 2, - url({ channel, date }) { - return `https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=${date.unix()}&endtime=${date - .add(1, 'd') - .unix()}&livechannelpids=${channel.site_id}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.Title, - description: item.Description, - icon: parseIcon(item), - start: parseTime(item.Start), - stop: parseTime(item.End) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get( - 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/contents/all?contentTypes=LCH&fields=Pid,Name&orderBy=contentOrder&limit=1000' - ) - .then(r => r.data.Content.List) - .catch(console.error) - - return items.map(i => { - return { - lang: 'es', - name: i.Name, - site_id: i.Pid.toLowerCase() - } - }) - } -} - -function parseIcon(item) { - return item.Images?.VideoFrame?.[0]?.Url -} - -function parseTime(timestamp) { - return dayjs.unix(timestamp) -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.Content || [] -} +const dayjs = require('dayjs') +const axios = require('axios') + +module.exports = { + site: 'tv.movistar.com.pe', + days: 2, + url({ channel, date }) { + return `https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=${date.unix()}&endtime=${date + .add(1, 'd') + .unix()}&livechannelpids=${channel.site_id}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.Title, + description: item.Description, + icon: parseIcon(item), + start: parseTime(item.Start), + stop: parseTime(item.End) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get( + 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/contents/all?contentTypes=LCH&fields=Pid,Name&orderBy=contentOrder&limit=1000' + ) + .then(r => r.data.Content.List) + .catch(console.error) + + return items.map(i => { + return { + lang: 'es', + name: i.Name, + site_id: i.Pid.toLowerCase() + } + }) + } +} + +function parseIcon(item) { + return item.Images?.VideoFrame?.[0]?.Url +} + +function parseTime(timestamp) { + return dayjs.unix(timestamp) +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.Content || [] +} diff --git a/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js b/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js index ac12544ad..28c957ca4 100644 --- a/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js +++ b/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js @@ -1,47 +1,47 @@ -// npm run channels:parse -- --config=./sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js --output=./sites/tv.movistar.com.pe/tv.movistar.com.pe.channels.xml -// npm run grab -- --site=tv.movistar.com.pe - -const { parser, url } = require('./tv.movistar.com.pe.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lch2219', - xmltv_id: 'WillaxTV.pe' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=1669680000&endtime=1669766400&livechannelpids=lch2219' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-28T23:50:00.000Z', - stop: '2022-11-29T00:50:00.000Z', - title: 'Willax noticias edición central', - description: - 'Edición central con el desarrollo y cobertura noticiosa de todos los acontecimientos nacionales e internacionales.', - icon: 'http://media.gvp.telefonica.com/storagearea0/IMAGES/00/13/00/13003906_281B2DAB18B01955.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js --output=./sites/tv.movistar.com.pe/tv.movistar.com.pe.channels.xml +// npm run grab -- --site=tv.movistar.com.pe + +const { parser, url } = require('./tv.movistar.com.pe.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lch2219', + xmltv_id: 'WillaxTV.pe' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=1669680000&endtime=1669766400&livechannelpids=lch2219' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-28T23:50:00.000Z', + stop: '2022-11-29T00:50:00.000Z', + title: 'Willax noticias edición central', + description: + 'Edición central con el desarrollo y cobertura noticiosa de todos los acontecimientos nacionales e internacionales.', + icon: 'http://media.gvp.telefonica.com/storagearea0/IMAGES/00/13/00/13003906_281B2DAB18B01955.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.nu/tv.nu.config.js b/sites/tv.nu/tv.nu.config.js index 2469fe220..a0c5d91ec 100644 --- a/sites/tv.nu/tv.nu.config.js +++ b/sites/tv.nu/tv.nu.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.nu', - days: 2, - url: function ({ channel, date }) { - return `https://web-api.tv.nu/channels/${channel.site_id}/schedule?date=${date.format( - 'YYYY-MM-DD' - )}&fullDay=true` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - icon: item.imageLandscape, - category: item.genres, - season: item.seasonNumber || null, - episode: item.episodeNumber || null, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - if (!item.broadcast || !item.broadcast.startTime) return null - - return dayjs(item.broadcast.startTime) -} - -function parseStop(item) { - if (!item.broadcast || !item.broadcast.endTime) return null - - return dayjs(item.broadcast.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.data || !Array.isArray(data.data.broadcasts)) return [] - - return data.data.broadcasts -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.nu', + days: 2, + url: function ({ channel, date }) { + return `https://web-api.tv.nu/channels/${channel.site_id}/schedule?date=${date.format( + 'YYYY-MM-DD' + )}&fullDay=true` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + icon: item.imageLandscape, + category: item.genres, + season: item.seasonNumber || null, + episode: item.episodeNumber || null, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + if (!item.broadcast || !item.broadcast.startTime) return null + + return dayjs(item.broadcast.startTime) +} + +function parseStop(item) { + if (!item.broadcast || !item.broadcast.endTime) return null + + return dayjs(item.broadcast.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.data || !Array.isArray(data.data.broadcasts)) return [] + + return data.data.broadcasts +} diff --git a/sites/tv.nu/tv.nu.test.js b/sites/tv.nu/tv.nu.test.js index af881a5f9..5170f29a2 100644 --- a/sites/tv.nu/tv.nu.test.js +++ b/sites/tv.nu/tv.nu.test.js @@ -1,51 +1,51 @@ -// npm run grab -- --site=tv.nu - -const { parser, url } = require('./tv.nu.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3sat', - xmltv_id: '3sat.de' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://web-api.tv.nu/channels/3sat/schedule?date=2022-03-06&fullDay=true' - ) -}) - -it('can parse response', () => { - const content = - '{"meta":{"status":200},"data":{"id":30139,"name":"RTL","slug":"rtl","themedLogo":{"light":{"url":"https://new.static.tv.nu/19402170","isFallback":false},"dark":{"url":"https://new.static.tv.nu/59995595","isFallback":true}},"broadcasts":[{"type":"broadcast","id":"1OoSZY-7Q7-1DzQ","slug":"csi","programId":"2452","isPlay":true,"isMovie":false,"isSeries":true,"isLive":false,"title":"CSI: Den Tätern auf der Spur","description":"Hellseherin Sedona Wiley wird tot aufgefunden. Die Ermittlungen führen zu einem alten Mord. Gordon Wallace wurde vor 15 Jahren beschuldigt, seine Frau getötet zu haben, jedoch wurde nie eine Leiche gefunden.","imagePortrait":"https://new.static.tv.nu/16686512","imageLandscape":"https://new.static.tv.nu/13119997","year":2006,"genres":["Action","Kriminaldrama","Mysterium","Spänning","Thriller"],"imdb":{"rating":"7.7","link":"https://www.imdb.com/title/tt0247082"},"playProviders":[{"name":"Viaplay","slug":"viaplay","themedLogo":{"light":{"url":"https://new.static.tv.nu/17048879","isFallback":false},"dark":{"url":"https://new.static.tv.nu/119659437","isFallback":false}},"url":"https://viaplay.se/serier/csi-crime-scene-investigation/sasong-6/avsnitt-19?utm_source=tv.nu&utm_content=CSI%3A+Crime+Scene+Investigation"},{"name":"Tele2 Play","slug":"tele2play","themedLogo":{"light":{"url":"https://new.static.tv.nu/158747195","isFallback":false},"dark":{"url":"https://new.static.tv.nu/158747194","isFallback":false}},"url":"https://www.comhemplay.se/open/vod/SH016259780000?utm_source=tv.nu&utm_medium=partner&utm_campaign=tabla&utm_content=CSI%3A+Crime+Scene+Investigation"},{"name":"Prime Video","slug":"prime-video","themedLogo":{"light":{"url":"https://new.static.tv.nu/23085972","isFallback":false},"dark":{"url":"https://new.static.tv.nu/275111","isFallback":true}},"url":"https://app.primevideo.com/detail?gti=amzn1.dv.gti.54af67f9-e58f-e6db-4991-81eb4f2efa37&utm_source=tv.nu"}],"broadcast":{"id":"1OoSZY-7Q7-1DzQ","startTime":1660878900000,"endTime":1660881600000,"channel":{"name":"RTL","slug":"rtl","themedLogo":{"light":{"url":"https://new.static.tv.nu/19402170","isFallback":false},"dark":{"url":"https://new.static.tv.nu/59995595","isFallback":true}}}},"totalEpisodes":24,"episodeNumber":19,"seasonNumber":6}]}}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-19T03:15:00.000Z', - stop: '2022-08-19T04:00:00.000Z', - title: 'CSI: Den Tätern auf der Spur', - description: - 'Hellseherin Sedona Wiley wird tot aufgefunden. Die Ermittlungen führen zu einem alten Mord. Gordon Wallace wurde vor 15 Jahren beschuldigt, seine Frau getötet zu haben, jedoch wurde nie eine Leiche gefunden.', - icon: 'https://new.static.tv.nu/13119997', - category: ['Action', 'Kriminaldrama', 'Mysterium', 'Spänning', 'Thriller'], - season: 6, - episode: 19 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"meta":{"status":200},"data":{"broadcasts":[]}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tv.nu + +const { parser, url } = require('./tv.nu.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3sat', + xmltv_id: '3sat.de' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://web-api.tv.nu/channels/3sat/schedule?date=2022-03-06&fullDay=true' + ) +}) + +it('can parse response', () => { + const content = + '{"meta":{"status":200},"data":{"id":30139,"name":"RTL","slug":"rtl","themedLogo":{"light":{"url":"https://new.static.tv.nu/19402170","isFallback":false},"dark":{"url":"https://new.static.tv.nu/59995595","isFallback":true}},"broadcasts":[{"type":"broadcast","id":"1OoSZY-7Q7-1DzQ","slug":"csi","programId":"2452","isPlay":true,"isMovie":false,"isSeries":true,"isLive":false,"title":"CSI: Den Tätern auf der Spur","description":"Hellseherin Sedona Wiley wird tot aufgefunden. Die Ermittlungen führen zu einem alten Mord. Gordon Wallace wurde vor 15 Jahren beschuldigt, seine Frau getötet zu haben, jedoch wurde nie eine Leiche gefunden.","imagePortrait":"https://new.static.tv.nu/16686512","imageLandscape":"https://new.static.tv.nu/13119997","year":2006,"genres":["Action","Kriminaldrama","Mysterium","Spänning","Thriller"],"imdb":{"rating":"7.7","link":"https://www.imdb.com/title/tt0247082"},"playProviders":[{"name":"Viaplay","slug":"viaplay","themedLogo":{"light":{"url":"https://new.static.tv.nu/17048879","isFallback":false},"dark":{"url":"https://new.static.tv.nu/119659437","isFallback":false}},"url":"https://viaplay.se/serier/csi-crime-scene-investigation/sasong-6/avsnitt-19?utm_source=tv.nu&utm_content=CSI%3A+Crime+Scene+Investigation"},{"name":"Tele2 Play","slug":"tele2play","themedLogo":{"light":{"url":"https://new.static.tv.nu/158747195","isFallback":false},"dark":{"url":"https://new.static.tv.nu/158747194","isFallback":false}},"url":"https://www.comhemplay.se/open/vod/SH016259780000?utm_source=tv.nu&utm_medium=partner&utm_campaign=tabla&utm_content=CSI%3A+Crime+Scene+Investigation"},{"name":"Prime Video","slug":"prime-video","themedLogo":{"light":{"url":"https://new.static.tv.nu/23085972","isFallback":false},"dark":{"url":"https://new.static.tv.nu/275111","isFallback":true}},"url":"https://app.primevideo.com/detail?gti=amzn1.dv.gti.54af67f9-e58f-e6db-4991-81eb4f2efa37&utm_source=tv.nu"}],"broadcast":{"id":"1OoSZY-7Q7-1DzQ","startTime":1660878900000,"endTime":1660881600000,"channel":{"name":"RTL","slug":"rtl","themedLogo":{"light":{"url":"https://new.static.tv.nu/19402170","isFallback":false},"dark":{"url":"https://new.static.tv.nu/59995595","isFallback":true}}}},"totalEpisodes":24,"episodeNumber":19,"seasonNumber":6}]}}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-19T03:15:00.000Z', + stop: '2022-08-19T04:00:00.000Z', + title: 'CSI: Den Tätern auf der Spur', + description: + 'Hellseherin Sedona Wiley wird tot aufgefunden. Die Ermittlungen führen zu einem alten Mord. Gordon Wallace wurde vor 15 Jahren beschuldigt, seine Frau getötet zu haben, jedoch wurde nie eine Leiche gefunden.', + icon: 'https://new.static.tv.nu/13119997', + category: ['Action', 'Kriminaldrama', 'Mysterium', 'Spänning', 'Thriller'], + season: 6, + episode: 19 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"meta":{"status":200},"data":{"broadcasts":[]}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.post.lu/tv.post.lu.config.js b/sites/tv.post.lu/tv.post.lu.config.js index cc223270a..74162f9a6 100644 --- a/sites/tv.post.lu/tv.post.lu.config.js +++ b/sites/tv.post.lu/tv.post.lu.config.js @@ -1,56 +1,56 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.post.lu', - days: 2, - url({ channel, date }) { - return `https://tv.post.lu/api/channels?id=${channel.site_id}&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - category: item.program_type, - icon: item.image_url, - start: dayjs.unix(item.tsStart), - stop: dayjs.unix(item.tsEnd) - }) - }) - - return programs - }, - async channels() { - const promises = [...Array(17).keys()].map(i => - axios.get(`https://tv.post.lu/api/channels/?page=${i + 1}`) - ) - - const channels = [] - await Promise.all(promises).then(values => { - values.forEach(r => { - let items = r.data.result.data - items.forEach(item => { - channels.push({ - lang: item.language.code, - name: item.name, - site_id: item.id - }) - }) - }) - }) - - return channels - } -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !data.result || !data.result.epg || !Array.isArray(data.result.epg.programme)) - return [] - - return data.result.epg.programme -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.post.lu', + days: 2, + url({ channel, date }) { + return `https://tv.post.lu/api/channels?id=${channel.site_id}&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + category: item.program_type, + icon: item.image_url, + start: dayjs.unix(item.tsStart), + stop: dayjs.unix(item.tsEnd) + }) + }) + + return programs + }, + async channels() { + const promises = [...Array(17).keys()].map(i => + axios.get(`https://tv.post.lu/api/channels/?page=${i + 1}`) + ) + + const channels = [] + await Promise.all(promises).then(values => { + values.forEach(r => { + let items = r.data.result.data + items.forEach(item => { + channels.push({ + lang: item.language.code, + name: item.name, + site_id: item.id + }) + }) + }) + }) + + return channels + } +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !data.result || !data.result.epg || !Array.isArray(data.result.epg.programme)) + return [] + + return data.result.epg.programme +} diff --git a/sites/tv.post.lu/tv.post.lu.test.js b/sites/tv.post.lu/tv.post.lu.test.js index 49aedbb48..877745b4d 100644 --- a/sites/tv.post.lu/tv.post.lu.test.js +++ b/sites/tv.post.lu/tv.post.lu.test.js @@ -1,49 +1,49 @@ -// npm run channels:parse -- --config=./sites/tv.post.lu/tv.post.lu.config.js --output=./sites/tv.post.lu/tv.post.lu.channels.xml -// npm run grab -- --site=tv.post.lu - -const { parser, url } = require('./tv.post.lu.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '269695d0-8076-11e9-b5ca-f345a2ed0fbe', - xmltv_id: 'DasErste.de' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv.post.lu/api/channels?id=269695d0-8076-11e9-b5ca-f345a2ed0fbe&date=2023-01-16' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Tagesschau', - description: - 'Das Flaggschiff unter den deutschen Nachrichtensendungen ist gleichzeitig die "dienstälteste" noch bestehende Sendung im deutschen Fernsehen. In bis zu 20 am Tag produzierten Sendungen wird die Komplexität des Weltgeschehens verständlich erklärt und in komprimierter Form über aktuelle politische, wirtschaftliche, soziale, kulturelle, sportliche und sonstige Ereignisse berichtet.', - category: 'Nachrichten', - icon: 'https://mp-photos-cdn.azureedge.net/container3cc71e4948ac40ab803c26e0abc2e3e5/original/e6eb49013a822f5c6eb2e7701e69a1f80aa0b947.jpg', - start: '2023-01-16T00:05:00.000Z', - stop: '2023-01-16T00:10:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv.post.lu/tv.post.lu.config.js --output=./sites/tv.post.lu/tv.post.lu.channels.xml +// npm run grab -- --site=tv.post.lu + +const { parser, url } = require('./tv.post.lu.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '269695d0-8076-11e9-b5ca-f345a2ed0fbe', + xmltv_id: 'DasErste.de' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv.post.lu/api/channels?id=269695d0-8076-11e9-b5ca-f345a2ed0fbe&date=2023-01-16' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Tagesschau', + description: + 'Das Flaggschiff unter den deutschen Nachrichtensendungen ist gleichzeitig die "dienstälteste" noch bestehende Sendung im deutschen Fernsehen. In bis zu 20 am Tag produzierten Sendungen wird die Komplexität des Weltgeschehens verständlich erklärt und in komprimierter Form über aktuelle politische, wirtschaftliche, soziale, kulturelle, sportliche und sonstige Ereignisse berichtet.', + category: 'Nachrichten', + icon: 'https://mp-photos-cdn.azureedge.net/container3cc71e4948ac40ab803c26e0abc2e3e5/original/e6eb49013a822f5c6eb2e7701e69a1f80aa0b947.jpg', + start: '2023-01-16T00:05:00.000Z', + stop: '2023-01-16T00:10:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tv.trueid.net/tv.trueid.net.config.js b/sites/tv.trueid.net/tv.trueid.net.config.js index a1dbbb5da..a8adb28f8 100644 --- a/sites/tv.trueid.net/tv.trueid.net.config.js +++ b/sites/tv.trueid.net/tv.trueid.net.config.js @@ -1,66 +1,66 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - delay: 5000, - site: 'tv.trueid.net', - days: 2, - url: function ({ channel, date }) { - return `https://tv.trueid.net/tvguide/all/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - request: { - jar: null - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.detail ? item.detail.thumb : null -} - -function parseStart(item) { - return item.detail ? dayjs.utc(item.detail.start_date) : null -} - -function parseStop(item) { - return item.detail ? dayjs.utc(item.detail.end_date) : null -} - -function parseContent(content, channel) { - const $ = cheerio.load(content) - const nextData = $('#__NEXT_DATA__').html() - const data = JSON.parse(nextData) - if ( - !data || - !data.props || - !data.props.pageProps || - !data.props.pageProps.listEPG || - !Array.isArray(data.props.pageProps.listEPG.data) - ) - return null - - return data.props.pageProps.listEPG.data.find(ch => ch.slug === channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - if (!data || !Array.isArray(data.programList)) return [] - - return data.programList -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + delay: 5000, + site: 'tv.trueid.net', + days: 2, + url: function ({ channel, date }) { + return `https://tv.trueid.net/tvguide/all/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + request: { + jar: null + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.detail ? item.detail.thumb : null +} + +function parseStart(item) { + return item.detail ? dayjs.utc(item.detail.start_date) : null +} + +function parseStop(item) { + return item.detail ? dayjs.utc(item.detail.end_date) : null +} + +function parseContent(content, channel) { + const $ = cheerio.load(content) + const nextData = $('#__NEXT_DATA__').html() + const data = JSON.parse(nextData) + if ( + !data || + !data.props || + !data.props.pageProps || + !data.props.pageProps.listEPG || + !Array.isArray(data.props.pageProps.listEPG.data) + ) + return null + + return data.props.pageProps.listEPG.data.find(ch => ch.slug === channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + if (!data || !Array.isArray(data.programList)) return [] + + return data.programList +} diff --git a/sites/tv.trueid.net/tv.trueid.net.test.js b/sites/tv.trueid.net/tv.trueid.net.test.js index 771243579..ebed99d7f 100644 --- a/sites/tv.trueid.net/tv.trueid.net.test.js +++ b/sites/tv.trueid.net/tv.trueid.net.test.js @@ -1,43 +1,43 @@ -// NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/tv.trueid.net/tv.trueid.net.config.js --channels=sites/tv.trueid.net/tv.trueid.net.channels.xml --output=guide.xml --timeout=30000 --days=2 - -const { parser, url } = require('./tv.trueid.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tv-nfl-nba', - xmltv_id: 'NFLNBATV.us', - name: 'NFL & NBA TV' -} -const content = - '' - -it('can generate valid url', () => { - const result = url({ channel, date }) - expect(result).toBe('https://tv.trueid.net/tvguide/all/tv-nfl-nba/2021-10-28') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-10-28T19:00:00.000Z', - stop: '2021-10-28T21:00:00.000Z', - title: 'NBA 2021/22', - icon: 'https://epgthumb.dmpcdn.com/thumbnail_large/t513/20211029/20211029_020000.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{}' }) - expect(result).toMatchObject([]) -}) +// NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/tv.trueid.net/tv.trueid.net.config.js --channels=sites/tv.trueid.net/tv.trueid.net.channels.xml --output=guide.xml --timeout=30000 --days=2 + +const { parser, url } = require('./tv.trueid.net.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tv-nfl-nba', + xmltv_id: 'NFLNBATV.us', + name: 'NFL & NBA TV' +} +const content = + '' + +it('can generate valid url', () => { + const result = url({ channel, date }) + expect(result).toBe('https://tv.trueid.net/tvguide/all/tv-nfl-nba/2021-10-28') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-10-28T19:00:00.000Z', + stop: '2021-10-28T21:00:00.000Z', + title: 'NBA 2021/22', + icon: 'https://epgthumb.dmpcdn.com/thumbnail_large/t513/20211029/20211029_020000.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.vera.com.uy/tv.vera.com.uy.config.js b/sites/tv.vera.com.uy/tv.vera.com.uy.config.js index 2f58fe9b3..c321be386 100644 --- a/sites/tv.vera.com.uy/tv.vera.com.uy.config.js +++ b/sites/tv.vera.com.uy/tv.vera.com.uy.config.js @@ -1,108 +1,108 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos' - -module.exports = { - site: 'tv.vera.com.uy', - days: 2, - async url({ date, channel }) { - const session = await loadSessionDetails() - if (!session || !session.token) return null - - return `${API_ENDPOINT}/canales/epg/${ - channel.site_id - }?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}` - }, - request: { - async headers() { - const session = await loadSessionDetails() - if (!session || !session.jwt) return null - - return { - authorization: `Bearer ${session.jwt}`, - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - } - } - }, - parser({ content }) { - let programs = [] - let items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.nombre_programa, - sub_title: item.subtitle, - description: item.descripcion_programa, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const session = await loadSessionDetails() - if (!session || !session.jwt || !session.token) return null - - const data = await axios - .get(`${API_ENDPOINT}/listas/68?token=${session.token}`, { - headers: { - authorization: `Bearer ${session.jwt}`, - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - } - }) - .then(r => r.data) - .catch(console.error) - - return data.contenidos.map(c => { - return { - lang: 'es', - site_id: c.public_id, - name: c.nombre - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseStop(item) { - return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data)) return [] - - return data.data -} - -function loadSessionDetails() { - return axios - .post( - 'https://veratv-be.vera.com.uy/api/sesiones', - { - tipo: 'anonima' - }, - { - headers: { - 'Content-Type': 'application/json' - } - } - ) - .then(r => r.data) - .catch(console.log) -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos' + +module.exports = { + site: 'tv.vera.com.uy', + days: 2, + async url({ date, channel }) { + const session = await loadSessionDetails() + if (!session || !session.token) return null + + return `${API_ENDPOINT}/canales/epg/${ + channel.site_id + }?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}` + }, + request: { + async headers() { + const session = await loadSessionDetails() + if (!session || !session.jwt) return null + + return { + authorization: `Bearer ${session.jwt}`, + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + } + } + }, + parser({ content }) { + let programs = [] + let items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.nombre_programa, + sub_title: item.subtitle, + description: item.descripcion_programa, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const session = await loadSessionDetails() + if (!session || !session.jwt || !session.token) return null + + const data = await axios + .get(`${API_ENDPOINT}/listas/68?token=${session.token}`, { + headers: { + authorization: `Bearer ${session.jwt}`, + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + } + }) + .then(r => r.data) + .catch(console.error) + + return data.contenidos.map(c => { + return { + lang: 'es', + site_id: c.public_id, + name: c.nombre + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseStop(item) { + return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data)) return [] + + return data.data +} + +function loadSessionDetails() { + return axios + .post( + 'https://veratv-be.vera.com.uy/api/sesiones', + { + tipo: 'anonima' + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + .then(r => r.data) + .catch(console.log) +} diff --git a/sites/tv.vera.com.uy/tv.vera.com.uy.test.js b/sites/tv.vera.com.uy/tv.vera.com.uy.test.js index 9cdfb281a..075f1ea99 100644 --- a/sites/tv.vera.com.uy/tv.vera.com.uy.test.js +++ b/sites/tv.vera.com.uy/tv.vera.com.uy.test.js @@ -1,88 +1,88 @@ -// npm run channels:parse -- --config=./sites/tv.vera.com.uy/tv.vera.com.uy.config.js --output=./sites/tv.vera.com.uy/tv.vera.com.uy.channels.xml -// npm run grab -- --site=tv.vera.com.uy - -const { parser, url, request } = require('./tv.vera.com.uy.config.js') -const fs = require('fs') -const axios = require('axios') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -axios.post.mockImplementation((url, data, opts) => { - if ( - url === 'https://veratv-be.vera.com.uy/api/sesiones' && - JSON.stringify(opts.headers) === - JSON.stringify({ - 'Content-Type': 'application/json' - }) && - JSON.stringify(data) === - JSON.stringify({ - tipo: 'anonima' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) - }) - } else { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) - }) - } -}) - -const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2s6nd', - xmltv_id: 'Canal5.uy' -} - -it('can generate valid url', async () => { - const result = await url({ date, channel }) - - expect(result).toBe( - 'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B' - ) -}) - -it('can generate valid request headers', async () => { - const result = await request.headers() - - expect(result).toMatchObject({ - authorization: - 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s', - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-11T02:30:00.000Z', - stop: '2023-02-11T04:00:00.000Z', - title: 'Canal 5 Noticias rep.', - sub_title: '', - description: '' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv.vera.com.uy/tv.vera.com.uy.config.js --output=./sites/tv.vera.com.uy/tv.vera.com.uy.channels.xml +// npm run grab -- --site=tv.vera.com.uy + +const { parser, url, request } = require('./tv.vera.com.uy.config.js') +const fs = require('fs') +const axios = require('axios') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +axios.post.mockImplementation((url, data, opts) => { + if ( + url === 'https://veratv-be.vera.com.uy/api/sesiones' && + JSON.stringify(opts.headers) === + JSON.stringify({ + 'Content-Type': 'application/json' + }) && + JSON.stringify(data) === + JSON.stringify({ + tipo: 'anonima' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) + }) + } else { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) + }) + } +}) + +const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2s6nd', + xmltv_id: 'Canal5.uy' +} + +it('can generate valid url', async () => { + const result = await url({ date, channel }) + + expect(result).toBe( + 'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B' + ) +}) + +it('can generate valid request headers', async () => { + const result = await request.headers() + + expect(result).toMatchObject({ + authorization: + 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s', + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-11T02:30:00.000Z', + stop: '2023-02-11T04:00:00.000Z', + title: 'Canal 5 Noticias rep.', + sub_title: '', + description: '' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tv.yandex.ru/tv.yandex.ru.config.js b/sites/tv.yandex.ru/tv.yandex.ru.config.js index c5883e4ca..ac42f0cdf 100644 --- a/sites/tv.yandex.ru/tv.yandex.ru.config.js +++ b/sites/tv.yandex.ru/tv.yandex.ru.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.yandex.ru', - days: 2, - url: function ({ date, channel }) { - const [region, id] = channel.site_id.split('#') - - return `https://tv.yandex.ru/${region}/channel/${id}?date=${date.format('YYYY-MM-DD')}` - }, - request: { - headers: { - Cookie: - 'yandexuid=8747786251615498142; Expires=Tue, 11 Mar 2031 21:29:02 GMT; Domain=yandex.ru; Path=/' - } - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.program.description, - category: item.program.type.name, - start: dayjs(item.start), - stop: dayjs(item.finish) - }) - }) - - return programs - } -} - -function parseContent(content) { - const [, initialState] = content.match(/window.__INITIAL_STATE__ = (.*);/i) || [null, null] - if (!initialState) return null - const data = JSON.parse(initialState) - if (!data) return null - - return data.channel -} - -function parseItems(content) { - const data = parseContent(content) - if (!data || !data.schedule || !Array.isArray(data.schedule.events)) return [] - - return data.schedule.events -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.yandex.ru', + days: 2, + url: function ({ date, channel }) { + const [region, id] = channel.site_id.split('#') + + return `https://tv.yandex.ru/${region}/channel/${id}?date=${date.format('YYYY-MM-DD')}` + }, + request: { + headers: { + Cookie: + 'yandexuid=8747786251615498142; Expires=Tue, 11 Mar 2031 21:29:02 GMT; Domain=yandex.ru; Path=/' + } + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.program.description, + category: item.program.type.name, + start: dayjs(item.start), + stop: dayjs(item.finish) + }) + }) + + return programs + } +} + +function parseContent(content) { + const [, initialState] = content.match(/window.__INITIAL_STATE__ = (.*);/i) || [null, null] + if (!initialState) return null + const data = JSON.parse(initialState) + if (!data) return null + + return data.channel +} + +function parseItems(content) { + const data = parseContent(content) + if (!data || !data.schedule || !Array.isArray(data.schedule.events)) return [] + + return data.schedule.events +} diff --git a/sites/tv.yandex.ru/tv.yandex.ru.test.js b/sites/tv.yandex.ru/tv.yandex.ru.test.js index 74112b07e..d1a65bc0c 100644 --- a/sites/tv.yandex.ru/tv.yandex.ru.test.js +++ b/sites/tv.yandex.ru/tv.yandex.ru.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=tv.yandex.ru - -const { parser, url, request } = require('./tv.yandex.ru.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '162#31-kanal-429', - xmltv_id: '31Kanal.kz' -} -const content = - ' ' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv.yandex.ru/162/channel/31-kanal-429?date=2021-11-25' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Cookie: - 'yandexuid=8747786251615498142; Expires=Tue, 11 Mar 2031 21:29:02 GMT; Domain=yandex.ru; Path=/' - }) -}) - -it('can parse response', () => { - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-24T23:58:00.000Z', - title: 'Ризамын (каз.).', - category: 'досуг', - description: 'kLX6FVKAIiDCGBFE' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tv.yandex.ru + +const { parser, url, request } = require('./tv.yandex.ru.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '162#31-kanal-429', + xmltv_id: '31Kanal.kz' +} +const content = + ' ' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv.yandex.ru/162/channel/31-kanal-429?date=2021-11-25' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Cookie: + 'yandexuid=8747786251615498142; Expires=Tue, 11 Mar 2031 21:29:02 GMT; Domain=yandex.ru; Path=/' + }) +}) + +it('can parse response', () => { + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-24T23:58:00.000Z', + title: 'Ризамын (каз.).', + category: 'досуг', + description: 'kLX6FVKAIiDCGBFE' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.yettel.hu/tv.yettel.hu.config.js b/sites/tv.yettel.hu/tv.yettel.hu.config.js index 9e54b779e..95760b909 100644 --- a/sites/tv.yettel.hu/tv.yettel.hu.config.js +++ b/sites/tv.yettel.hu/tv.yettel.hu.config.js @@ -1,69 +1,69 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.yettel.hu', - days: 2, - url: function ({ channel, date }) { - return `https://dev.mytvback.com/api/19/default/hu-HU/schedules?livechannelpids=${ - channel.site_id - }&includeImages=cover%3A100%3A144&filterAvailability=false&startTime=${date.unix()}&endTime=${date - .add(1, 'd') - .unix()}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.Title, - description: item.ShortDescription, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://dev.mytvback.com/api/19/default/hu-HU/content/CHA_LIVE_MYTV2_HU/children') - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let item of data.Content.List) { - channels.push({ - lang: 'hu', - site_id: item.Pid, - name: item.CallLetter - }) - } - - return channels - } -} - -function parseIcon(item) { - if (Array.isArray(item.Images.Cover) && item.Images.Cover.length) { - return item.Images.Cover[0].Url - } - - return null -} - -function parseStart(item) { - return dayjs.unix(item.Start) -} - -function parseStop(item) { - return dayjs.unix(item.End) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.Content)) return [] - - return data.Content -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.yettel.hu', + days: 2, + url: function ({ channel, date }) { + return `https://dev.mytvback.com/api/19/default/hu-HU/schedules?livechannelpids=${ + channel.site_id + }&includeImages=cover%3A100%3A144&filterAvailability=false&startTime=${date.unix()}&endTime=${date + .add(1, 'd') + .unix()}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.Title, + description: item.ShortDescription, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://dev.mytvback.com/api/19/default/hu-HU/content/CHA_LIVE_MYTV2_HU/children') + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let item of data.Content.List) { + channels.push({ + lang: 'hu', + site_id: item.Pid, + name: item.CallLetter + }) + } + + return channels + } +} + +function parseIcon(item) { + if (Array.isArray(item.Images.Cover) && item.Images.Cover.length) { + return item.Images.Cover[0].Url + } + + return null +} + +function parseStart(item) { + return dayjs.unix(item.Start) +} + +function parseStop(item) { + return dayjs.unix(item.End) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.Content)) return [] + + return data.Content +} diff --git a/sites/tv.yettel.hu/tv.yettel.hu.test.js b/sites/tv.yettel.hu/tv.yettel.hu.test.js index f283d71c6..2cccbb7a1 100644 --- a/sites/tv.yettel.hu/tv.yettel.hu.test.js +++ b/sites/tv.yettel.hu/tv.yettel.hu.test.js @@ -1,83 +1,83 @@ -// npm run channels:parse -- --config=./sites/tv.yettel.hu/tv.yettel.hu.config.js --output=./sites/tv.yettel.hu/tv.yettel.hu.channels.xml -// npm run grab -- --site=tv.yettel.hu - -const { parser, url } = require('./tv.yettel.hu.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-06-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'LCH1', - xmltv_id: 'M1.hu' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://dev.mytvback.com/api/19/default/hu-HU/schedules?livechannelpids=LCH1&includeImages=cover%3A100%3A144&filterAvailability=false&startTime=1655424000&endTime=1655510400' - ) -}) - -it('can parse response', () => { - const content = `{ - "Content": [ - { - "AgeRatingPid": "", - "catchup_days": "0", - "AvailableUntil": 1655445600, - "Description": "", - "End": 1655445600, - "LiveChannelPid": "LCH1", - "ch_id": "1", - "LiveProgramPid": "LEP3906574", - "pr_id": "3906574", - "se_id": "13986", - "LiveSeriesPid": "LSE13986", - "Pid": "LSC17202373", - "id": "17202373", - "Rating": 0, - "RatingTotalVotes": 0, - "ShortDescription": "A Ma reggel az MTVA saját gyártású, minden hétköznap jelentkező reggeli politikai és közéleti témákkal foglalkozó műsora.", - "Start": 1655443980, - "Title": "Ma reggel", - "Year": 2022, - "GenrePids": [ - "GEN184" - ], - "ge_id": "184", - "IsCatchup": "1", - "ChannelIsCatchup": "0", - "Images": { - "Cover": [ - { - "Url": "https://static.mytvback.com/userfiles/c/0/c01d48a36b913a7afb0dcb5edba33849_thum_100x144.jpg" - } - ] - } - }]}` - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-06-17T05:33:00.000Z', - stop: '2022-06-17T06:00:00.000Z', - title: 'Ma reggel', - description: - 'A Ma reggel az MTVA saját gyártású, minden hétköznap jelentkező reggeli politikai és közéleti témákkal foglalkozó műsora.', - icon: 'https://static.mytvback.com/userfiles/c/0/c01d48a36b913a7afb0dcb5edba33849_thum_100x144.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"Content":[],"HttpStatusCode":200,"StatusCode":0,"StatusMessage":"OK","Severity":1}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv.yettel.hu/tv.yettel.hu.config.js --output=./sites/tv.yettel.hu/tv.yettel.hu.channels.xml +// npm run grab -- --site=tv.yettel.hu + +const { parser, url } = require('./tv.yettel.hu.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-06-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'LCH1', + xmltv_id: 'M1.hu' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://dev.mytvback.com/api/19/default/hu-HU/schedules?livechannelpids=LCH1&includeImages=cover%3A100%3A144&filterAvailability=false&startTime=1655424000&endTime=1655510400' + ) +}) + +it('can parse response', () => { + const content = `{ + "Content": [ + { + "AgeRatingPid": "", + "catchup_days": "0", + "AvailableUntil": 1655445600, + "Description": "", + "End": 1655445600, + "LiveChannelPid": "LCH1", + "ch_id": "1", + "LiveProgramPid": "LEP3906574", + "pr_id": "3906574", + "se_id": "13986", + "LiveSeriesPid": "LSE13986", + "Pid": "LSC17202373", + "id": "17202373", + "Rating": 0, + "RatingTotalVotes": 0, + "ShortDescription": "A Ma reggel az MTVA saját gyártású, minden hétköznap jelentkező reggeli politikai és közéleti témákkal foglalkozó műsora.", + "Start": 1655443980, + "Title": "Ma reggel", + "Year": 2022, + "GenrePids": [ + "GEN184" + ], + "ge_id": "184", + "IsCatchup": "1", + "ChannelIsCatchup": "0", + "Images": { + "Cover": [ + { + "Url": "https://static.mytvback.com/userfiles/c/0/c01d48a36b913a7afb0dcb5edba33849_thum_100x144.jpg" + } + ] + } + }]}` + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-06-17T05:33:00.000Z', + stop: '2022-06-17T06:00:00.000Z', + title: 'Ma reggel', + description: + 'A Ma reggel az MTVA saját gyártású, minden hétköznap jelentkező reggeli politikai és közéleti témákkal foglalkozó műsora.', + icon: 'https://static.mytvback.com/userfiles/c/0/c01d48a36b913a7afb0dcb5edba33849_thum_100x144.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"Content":[],"HttpStatusCode":200,"StatusCode":0,"StatusMessage":"OK","Severity":1}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv24.co.uk/tv24.co.uk.config.js b/sites/tv24.co.uk/tv24.co.uk.config.js index 6a9a7ff97..727555777 100644 --- a/sites/tv24.co.uk/tv24.co.uk.config.js +++ b/sites/tv24.co.uk/tv24.co.uk.config.js @@ -1,92 +1,92 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tv24.co.uk', - days: 2, - url: function ({ channel, date }) { - return `https://tv24.co.uk/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let html = await axios - .get('https://tv24.co.uk/x/settings/addremove') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - const nums = $('li') - .toArray() - .map(item => $(item).data('channel')) - html = await axios - .get('https://tv24.co.uk', { - headers: { - Cookie: `selectedChannels=${nums.join(',')}` - } - }) - .then(r => r.data) - .catch(console.log) - $ = cheerio.load(html) - const items = $('li.c').toArray() - - return items.map(item => { - const name = $(item).find('h3').text().trim() - const link = $(item).find('.channel').attr('href') - const [, site_id] = link.match(/\/channel\/(.*)/) || [null, null] - - return { - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('h3').text() -} - -function parseDescription($item) { - return $item('p').text() -} - -function parseStart($item, date) { - const time = $item('.time').text() - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h:mma') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.program').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tv24.co.uk', + days: 2, + url: function ({ channel, date }) { + return `https://tv24.co.uk/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let html = await axios + .get('https://tv24.co.uk/x/settings/addremove') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + const nums = $('li') + .toArray() + .map(item => $(item).data('channel')) + html = await axios + .get('https://tv24.co.uk', { + headers: { + Cookie: `selectedChannels=${nums.join(',')}` + } + }) + .then(r => r.data) + .catch(console.log) + $ = cheerio.load(html) + const items = $('li.c').toArray() + + return items.map(item => { + const name = $(item).find('h3').text().trim() + const link = $(item).find('.channel').attr('href') + const [, site_id] = link.match(/\/channel\/(.*)/) || [null, null] + + return { + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('h3').text() +} + +function parseDescription($item) { + return $item('p').text() +} + +function parseStart($item, date) { + const time = $item('.time').text() + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h:mma') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.program').toArray() +} diff --git a/sites/tv24.co.uk/tv24.co.uk.test.js b/sites/tv24.co.uk/tv24.co.uk.test.js index 3a569ab26..95bc72e37 100644 --- a/sites/tv24.co.uk/tv24.co.uk.test.js +++ b/sites/tv24.co.uk/tv24.co.uk.test.js @@ -1,53 +1,53 @@ -// npm run channels:parse -- --config=./sites/tv24.co.uk/tv24.co.uk.config.js --output=./sites/tv24.co.uk/tv24.co.uk.channels.xml -// npm run grab -- --site=tv24.co.uk - -const { parser, url } = require('./tv24.co.uk.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'bbc-two', - xmltv_id: 'BBCTwo.uk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv24.co.uk/x/channel/bbc-two/0/2022-08-28') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-28T05:05:00.000Z', - stop: '2022-08-28T06:05:00.000Z', - title: "Gardeners' World", - description: - 'Arit Anderson discovers a paradise garden in Cambridge which has become a focal point for the local community, and Frances Tophill shares the joy of collecting and saving heirloom vegetable seeds on a visit to Pembrokeshire.' - }) - - expect(results[22]).toMatchObject({ - start: '2022-08-29T05:30:00.000Z', - stop: '2022-08-29T06:00:00.000Z', - title: 'Animal Park', - description: - "One of the park's vultures has laid an egg. It is ten years since Longleat had a successfully reared vulture chick, so the keepers send Hamza to find out if the parents are incubating their egg." - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv24.co.uk/tv24.co.uk.config.js --output=./sites/tv24.co.uk/tv24.co.uk.channels.xml +// npm run grab -- --site=tv24.co.uk + +const { parser, url } = require('./tv24.co.uk.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'bbc-two', + xmltv_id: 'BBCTwo.uk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv24.co.uk/x/channel/bbc-two/0/2022-08-28') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-28T05:05:00.000Z', + stop: '2022-08-28T06:05:00.000Z', + title: "Gardeners' World", + description: + 'Arit Anderson discovers a paradise garden in Cambridge which has become a focal point for the local community, and Frances Tophill shares the joy of collecting and saving heirloom vegetable seeds on a visit to Pembrokeshire.' + }) + + expect(results[22]).toMatchObject({ + start: '2022-08-29T05:30:00.000Z', + stop: '2022-08-29T06:00:00.000Z', + title: 'Animal Park', + description: + "One of the park's vultures has laid an egg. It is ten years since Longleat had a successfully reared vulture chick, so the keepers send Hamza to find out if the parents are incubating their egg." + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv24.se/tv24.se.config.js b/sites/tv24.se/tv24.se.config.js index 1e981cc2f..832d6ec04 100644 --- a/sites/tv24.se/tv24.se.config.js +++ b/sites/tv24.se/tv24.se.config.js @@ -1,163 +1,163 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tv24.se', - days: 2, - url: function ({ channel, date }) { - return `https://tv24.se/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` - }, - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - const details = await loadProgramDetails($item) - programs.push({ - title: parseTitle($item), - description: details.description, - actors: details.actors, - icon: details.icon, - category: details.category, - sub_title: details.sub_title, - season: details.season, - episode: details.episode, - start, - stop - }) - } - - return programs - }, - async channels() { - let html = await axios - .get('https://tv24.se/x/settings/addremove') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - const nums = $('li') - .toArray() - .map(item => $(item).data('channel')) - html = await axios - .get('https://tv24.se', { - headers: { - Cookie: `selectedChannels=${nums.join(',')}` - } - }) - .then(r => r.data) - .catch(console.log) - $ = cheerio.load(html) - const items = $('li.c').toArray() - - return items.map(item => { - const name = $(item).find('h3').text().trim() - const link = $(item).find('.channel').attr('href') - const [, site_id] = link.match(/\/kanal\/(.*)/) || [null, null] - - return { - lang: 'sv', - site_id, - name - } - }) - } -} - -async function loadProgramDetails($item) { - const programId = $item('a').attr('href') - const data = await axios - .get(`https://tv24.se/x${programId}/0/0`) - .then(r => r.data) - .catch(console.error) - if (!data) return Promise.resolve({}) - const $ = cheerio.load(data.contentBefore + data.contentAfter) - - return Promise.resolve({ - icon: parseIcon($), - actors: parseActors($), - description: parseDescription($), - category: parseCategory($), - sub_title: parseSubTitle($), - season: parseSeason($), - episode: parseEpisode($) - }) -} - -function parseIcon($) { - const style = $('.image > .actual').attr('style') - const [, url] = style.match(/background-image: url\('([^']+)'\)/) - - return url -} - -function parseSeason($) { - const [, season] = $('.sub-title') - .text() - .trim() - .match(/Säsong (\d+)/) || [null, ''] - - return parseInt(season) -} - -function parseEpisode($) { - const [, episode] = $('.sub-title') - .text() - .trim() - .match(/Avsnitt (\d+)/) || [null, ''] - - return parseInt(episode) -} - -function parseSubTitle($) { - const [, subtitle] = $('.sub-title').text().trim().split(': ') - - return subtitle -} - -function parseCategory($) { - return $('.extras > dt:contains(Kategori)').next().text().trim().split(' / ') -} - -function parseActors($) { - return $('.cast > li') - .map((i, el) => { - return $(el).find('.name').text().trim() - }) - .get() -} - -function parseDescription($) { - return $('.info > p').text().trim() -} - -function parseTitle($item) { - return $item('h3').text() -} - -function parseStart($item, date) { - const time = $item('.time') - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.program').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tv24.se', + days: 2, + url: function ({ channel, date }) { + return `https://tv24.se/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` + }, + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + const details = await loadProgramDetails($item) + programs.push({ + title: parseTitle($item), + description: details.description, + actors: details.actors, + icon: details.icon, + category: details.category, + sub_title: details.sub_title, + season: details.season, + episode: details.episode, + start, + stop + }) + } + + return programs + }, + async channels() { + let html = await axios + .get('https://tv24.se/x/settings/addremove') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + const nums = $('li') + .toArray() + .map(item => $(item).data('channel')) + html = await axios + .get('https://tv24.se', { + headers: { + Cookie: `selectedChannels=${nums.join(',')}` + } + }) + .then(r => r.data) + .catch(console.log) + $ = cheerio.load(html) + const items = $('li.c').toArray() + + return items.map(item => { + const name = $(item).find('h3').text().trim() + const link = $(item).find('.channel').attr('href') + const [, site_id] = link.match(/\/kanal\/(.*)/) || [null, null] + + return { + lang: 'sv', + site_id, + name + } + }) + } +} + +async function loadProgramDetails($item) { + const programId = $item('a').attr('href') + const data = await axios + .get(`https://tv24.se/x${programId}/0/0`) + .then(r => r.data) + .catch(console.error) + if (!data) return Promise.resolve({}) + const $ = cheerio.load(data.contentBefore + data.contentAfter) + + return Promise.resolve({ + icon: parseIcon($), + actors: parseActors($), + description: parseDescription($), + category: parseCategory($), + sub_title: parseSubTitle($), + season: parseSeason($), + episode: parseEpisode($) + }) +} + +function parseIcon($) { + const style = $('.image > .actual').attr('style') + const [, url] = style.match(/background-image: url\('([^']+)'\)/) + + return url +} + +function parseSeason($) { + const [, season] = $('.sub-title') + .text() + .trim() + .match(/Säsong (\d+)/) || [null, ''] + + return parseInt(season) +} + +function parseEpisode($) { + const [, episode] = $('.sub-title') + .text() + .trim() + .match(/Avsnitt (\d+)/) || [null, ''] + + return parseInt(episode) +} + +function parseSubTitle($) { + const [, subtitle] = $('.sub-title').text().trim().split(': ') + + return subtitle +} + +function parseCategory($) { + return $('.extras > dt:contains(Kategori)').next().text().trim().split(' / ') +} + +function parseActors($) { + return $('.cast > li') + .map((i, el) => { + return $(el).find('.name').text().trim() + }) + .get() +} + +function parseDescription($) { + return $('.info > p').text().trim() +} + +function parseTitle($item) { + return $item('h3').text() +} + +function parseStart($item, date) { + const time = $item('.time') + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.program').toArray() +} diff --git a/sites/tv24.se/tv24.se.test.js b/sites/tv24.se/tv24.se.test.js index 8cd14fac7..24b6726e9 100644 --- a/sites/tv24.se/tv24.se.test.js +++ b/sites/tv24.se/tv24.se.test.js @@ -1,78 +1,78 @@ -// npm run channels:parse -- --config=./sites/tv24.se/tv24.se.config.js --output=./sites/tv24.se/tv24.se.channels.xml -// npm run grab -- --site=tv24.se - -const { parser, url } = require('./tv24.se.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-08-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'svt1', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv24.se/x/channel/svt1/0/2022-08-26') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - axios.get.mockImplementation(url => { - if (url === 'https://tv24.se/x/b/rh7f40-1hkm/0/0') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://tv24.se/x/b/rh9dhc-1hkm/0/0') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-26T04:00:00.000Z', - stop: '2022-08-26T07:10:00.000Z', - title: 'Morgonstudion', - icon: 'https://jrsy.tmsimg.com/assets/p14436175_i_h9_ad.jpg', - description: - 'Dagens viktigaste nyheter och analyser med ständiga uppdateringar. Vi sänder direkt inrikes- och utrikesnyheter inklusive sport, kultur och nöje. Dessutom intervjuer med aktuella gäster. Nyhetssammanfattningar varje kvart med start kl 06.00.', - actors: ['Carolina Neurath', 'Karin Magnusson', 'Pelle Nilsson', 'Ted Wigren'] - }) - - expect(results[33]).toMatchObject({ - start: '2022-08-27T05:20:00.000Z', - stop: '2022-08-27T05:50:00.000Z', - title: 'Uppdrag granskning', - icon: 'https://jrsy.tmsimg.com/assets/p22818697_e_h9_aa.jpg', - description: - 'När samtliga sex män frias för ännu en skjutning växer vreden inom polisen. Ökningen av skjutningar i Sverige ligger i topp i Europa - och nu är våldsspiralen på väg mot ett nattsvart rekord. Hur blev Sverige landet där mördare går fria?', - actors: ['Karin Mattisson', 'Ali Fegan'], - category: ['Dokumentär', 'Samhällsfrågor'], - season: 23, - episode: 5, - sub_title: 'Där mördare går fria' - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ content: '' }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv24.se/tv24.se.config.js --output=./sites/tv24.se/tv24.se.channels.xml +// npm run grab -- --site=tv24.se + +const { parser, url } = require('./tv24.se.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-08-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'svt1', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv24.se/x/channel/svt1/0/2022-08-26') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + axios.get.mockImplementation(url => { + if (url === 'https://tv24.se/x/b/rh7f40-1hkm/0/0') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://tv24.se/x/b/rh9dhc-1hkm/0/0') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-26T04:00:00.000Z', + stop: '2022-08-26T07:10:00.000Z', + title: 'Morgonstudion', + icon: 'https://jrsy.tmsimg.com/assets/p14436175_i_h9_ad.jpg', + description: + 'Dagens viktigaste nyheter och analyser med ständiga uppdateringar. Vi sänder direkt inrikes- och utrikesnyheter inklusive sport, kultur och nöje. Dessutom intervjuer med aktuella gäster. Nyhetssammanfattningar varje kvart med start kl 06.00.', + actors: ['Carolina Neurath', 'Karin Magnusson', 'Pelle Nilsson', 'Ted Wigren'] + }) + + expect(results[33]).toMatchObject({ + start: '2022-08-27T05:20:00.000Z', + stop: '2022-08-27T05:50:00.000Z', + title: 'Uppdrag granskning', + icon: 'https://jrsy.tmsimg.com/assets/p22818697_e_h9_aa.jpg', + description: + 'När samtliga sex män frias för ännu en skjutning växer vreden inom polisen. Ökningen av skjutningar i Sverige ligger i topp i Europa - och nu är våldsspiralen på väg mot ett nattsvart rekord. Hur blev Sverige landet där mördare går fria?', + actors: ['Karin Mattisson', 'Ali Fegan'], + category: ['Dokumentär', 'Samhällsfrågor'], + season: 23, + episode: 5, + sub_title: 'Där mördare går fria' + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv2go.t-2.net/jquery.md5.js b/sites/tv2go.t-2.net/jquery.md5.js index 43518dc67..a8f351dda 100644 --- a/sites/tv2go.t-2.net/jquery.md5.js +++ b/sites/tv2go.t-2.net/jquery.md5.js @@ -1,264 +1,264 @@ -/* - * jQuery MD5 Plugin 1.2.1 - * https://github.com/blueimp/jQuery-MD5 - * - * Copyright 2010, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://creativecommons.org/licenses/MIT/ - * - * Based on - * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message - * Digest Algorithm, as defined in RFC 1321. - * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 - * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet - * Distributed under the BSD License - * See http://pajhome.org.uk/crypt/md5 for more info. - */ - -/* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ -function safe_add(x, y) { - var lsw = (x & 0xffff) + (y & 0xffff), - msw = (x >> 16) + (y >> 16) + (lsw >> 16) - return (msw << 16) | (lsw & 0xffff) -} - -/* - * Bitwise rotate a 32-bit number to the left. - */ -function bit_rol(num, cnt) { - return (num << cnt) | (num >>> (32 - cnt)) -} - -/* - * These functions implement the four basic operations the algorithm uses. - */ -function md5_cmn(q, a, b, x, s, t) { - return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) -} -function md5_ff(a, b, c, d, x, s, t) { - return md5_cmn((b & c) | (~b & d), a, b, x, s, t) -} -function md5_gg(a, b, c, d, x, s, t) { - return md5_cmn((b & d) | (c & ~d), a, b, x, s, t) -} -function md5_hh(a, b, c, d, x, s, t) { - return md5_cmn(b ^ c ^ d, a, b, x, s, t) -} -function md5_ii(a, b, c, d, x, s, t) { - return md5_cmn(c ^ (b | ~d), a, b, x, s, t) -} - -/* - * Calculate the MD5 of an array of little-endian words, and a bit length. - */ -function binl_md5(x, len) { - /* append padding */ - x[len >> 5] |= 0x80 << len % 32 - x[(((len + 64) >>> 9) << 4) + 14] = len - - var i, - olda, - oldb, - oldc, - oldd, - a = 1732584193, - b = -271733879, - c = -1732584194, - d = 271733878 - - for (i = 0; i < x.length; i += 16) { - olda = a - oldb = b - oldc = c - oldd = d - - a = md5_ff(a, b, c, d, x[i], 7, -680876936) - d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586) - c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819) - b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330) - a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897) - d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426) - c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341) - b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983) - a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416) - d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417) - c = md5_ff(c, d, a, b, x[i + 10], 17, -42063) - b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162) - a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682) - d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101) - c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290) - b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329) - - a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510) - d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632) - c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713) - b = md5_gg(b, c, d, a, x[i], 20, -373897302) - a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691) - d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083) - c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335) - b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848) - a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438) - d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690) - c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961) - b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501) - a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467) - d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784) - c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473) - b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734) - - a = md5_hh(a, b, c, d, x[i + 5], 4, -378558) - d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463) - c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562) - b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556) - a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060) - d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353) - c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632) - b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640) - a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174) - d = md5_hh(d, a, b, c, x[i], 11, -358537222) - c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979) - b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189) - a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487) - d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835) - c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520) - b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651) - - a = md5_ii(a, b, c, d, x[i], 6, -198630844) - d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415) - c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905) - b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055) - a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571) - d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606) - c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523) - b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799) - a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359) - d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744) - c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380) - b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649) - a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070) - d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379) - c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259) - b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551) - - a = safe_add(a, olda) - b = safe_add(b, oldb) - c = safe_add(c, oldc) - d = safe_add(d, oldd) - } - return [a, b, c, d] -} - -/* - * Convert an array of little-endian words to a string - */ -function binl2rstr(input) { - var i, - output = '' - for (i = 0; i < input.length * 32; i += 8) { - output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff) - } - return output -} - -/* - * Convert a raw string to an array of little-endian words - * Characters >255 have their high-byte silently ignored. - */ -function rstr2binl(input) { - var i, - output = [] - output[(input.length >> 2) - 1] = undefined - for (i = 0; i < output.length; i += 1) { - output[i] = 0 - } - for (i = 0; i < input.length * 8; i += 8) { - output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32 - } - return output -} - -/* - * Calculate the MD5 of a raw string - */ -function rstr_md5(s) { - return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)) -} - -/* - * Calculate the HMAC-MD5, of a key and some data (raw strings) - */ -function rstr_hmac_md5(key, data) { - var i, - bkey = rstr2binl(key), - ipad = [], - opad = [], - hash - ipad[15] = opad[15] = undefined - if (bkey.length > 16) { - bkey = binl_md5(bkey, key.length * 8) - } - for (i = 0; i < 16; i += 1) { - ipad[i] = bkey[i] ^ 0x36363636 - opad[i] = bkey[i] ^ 0x5c5c5c5c - } - hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) - return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)) -} - -/* - * Convert a raw string to a hex string - */ -function rstr2hex(input) { - var hex_tab = '0123456789abcdef', - output = '', - x, - i - for (i = 0; i < input.length; i += 1) { - x = input.charCodeAt(i) - output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f) - } - return output -} - -/* - * Encode a string as utf-8 - */ -function str2rstr_utf8(input) { - return unescape(encodeURIComponent(input)) -} - -/* - * Take string arguments and return either raw or hex encoded strings - */ -function raw_md5(s) { - return rstr_md5(str2rstr_utf8(s)) -} -function hex_md5(s) { - return rstr2hex(raw_md5(s)) -} -function raw_hmac_md5(k, d) { - return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)) -} -function hex_hmac_md5(k, d) { - return rstr2hex(raw_hmac_md5(k, d)) -} - -module.exports = function (string, key, raw) { - if (!key) { - if (!raw) { - return hex_md5(string) - } else { - return raw_md5(string) - } - } - if (!raw) { - return hex_hmac_md5(key, string) - } else { - return raw_hmac_md5(key, string) - } -} +/* + * jQuery MD5 Plugin 1.2.1 + * https://github.com/blueimp/jQuery-MD5 + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://creativecommons.org/licenses/MIT/ + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) { + var lsw = (x & 0xffff) + (y & 0xffff), + msw = (x >> 16) + (y >> 16) + (lsw >> 16) + return (msw << 16) | (lsw & 0xffff) +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)) +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) { + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) +} +function md5_ff(a, b, c, d, x, s, t) { + return md5_cmn((b & c) | (~b & d), a, b, x, s, t) +} +function md5_gg(a, b, c, d, x, s, t) { + return md5_cmn((b & d) | (c & ~d), a, b, x, s, t) +} +function md5_hh(a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t) +} +function md5_ii(a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | ~d), a, b, x, s, t) +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ +function binl_md5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << len % 32 + x[(((len + 64) >>> 9) << 4) + 14] = len + + var i, + olda, + oldb, + oldc, + oldd, + a = 1732584193, + b = -271733879, + c = -1732584194, + d = 271733878 + + for (i = 0; i < x.length; i += 16) { + olda = a + oldb = b + oldc = c + oldd = d + + a = md5_ff(a, b, c, d, x[i], 7, -680876936) + d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586) + c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819) + b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330) + a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897) + d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426) + c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341) + b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983) + a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416) + d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417) + c = md5_ff(c, d, a, b, x[i + 10], 17, -42063) + b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162) + a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682) + d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101) + c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290) + b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329) + + a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510) + d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632) + c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713) + b = md5_gg(b, c, d, a, x[i], 20, -373897302) + a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691) + d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083) + c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335) + b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848) + a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438) + d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690) + c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961) + b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501) + a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467) + d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784) + c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473) + b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734) + + a = md5_hh(a, b, c, d, x[i + 5], 4, -378558) + d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463) + c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562) + b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556) + a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060) + d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353) + c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632) + b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640) + a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174) + d = md5_hh(d, a, b, c, x[i], 11, -358537222) + c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979) + b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189) + a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487) + d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835) + c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520) + b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651) + + a = md5_ii(a, b, c, d, x[i], 6, -198630844) + d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415) + c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905) + b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055) + a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571) + d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606) + c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523) + b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799) + a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359) + d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744) + c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380) + b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649) + a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070) + d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379) + c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259) + b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551) + + a = safe_add(a, olda) + b = safe_add(b, oldb) + c = safe_add(c, oldc) + d = safe_add(d, oldd) + } + return [a, b, c, d] +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2rstr(input) { + var i, + output = '' + for (i = 0; i < input.length * 32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff) + } + return output +} + +/* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ +function rstr2binl(input) { + var i, + output = [] + output[(input.length >> 2) - 1] = undefined + for (i = 0; i < output.length; i += 1) { + output[i] = 0 + } + for (i = 0; i < input.length * 8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32 + } + return output +} + +/* + * Calculate the MD5 of a raw string + */ +function rstr_md5(s) { + return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)) +} + +/* + * Calculate the HMAC-MD5, of a key and some data (raw strings) + */ +function rstr_hmac_md5(key, data) { + var i, + bkey = rstr2binl(key), + ipad = [], + opad = [], + hash + ipad[15] = opad[15] = undefined + if (bkey.length > 16) { + bkey = binl_md5(bkey, key.length * 8) + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636 + opad[i] = bkey[i] ^ 0x5c5c5c5c + } + hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) + return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)) +} + +/* + * Convert a raw string to a hex string + */ +function rstr2hex(input) { + var hex_tab = '0123456789abcdef', + output = '', + x, + i + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i) + output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f) + } + return output +} + +/* + * Encode a string as utf-8 + */ +function str2rstr_utf8(input) { + return unescape(encodeURIComponent(input)) +} + +/* + * Take string arguments and return either raw or hex encoded strings + */ +function raw_md5(s) { + return rstr_md5(str2rstr_utf8(s)) +} +function hex_md5(s) { + return rstr2hex(raw_md5(s)) +} +function raw_hmac_md5(k, d) { + return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)) +} +function hex_hmac_md5(k, d) { + return rstr2hex(raw_hmac_md5(k, d)) +} + +module.exports = function (string, key, raw) { + if (!key) { + if (!raw) { + return hex_md5(string) + } else { + return raw_md5(string) + } + } + if (!raw) { + return hex_hmac_md5(key, string) + } else { + return raw_hmac_md5(key, string) + } +} diff --git a/sites/tv2go.t-2.net/tv2go.t-2.net.config.js b/sites/tv2go.t-2.net/tv2go.t-2.net.config.js index 53300e5f0..2391d951c 100644 --- a/sites/tv2go.t-2.net/tv2go.t-2.net.config.js +++ b/sites/tv2go.t-2.net/tv2go.t-2.net.config.js @@ -1,126 +1,126 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const md5 = require('./jquery.md5') - -const API = { - locale: 'sl-SI', - version: '9.4', - format: 'json', - uuid: '464830403846070', - token: '6dace810-55d5-11e3-949a-0800200c9a66' -} - -const config = { - site: 'tv2go.t-2.net', - days: 2, - url({ date, channel }) { - const data = config.request.data({ date, channel }) - const endpoint = 'client/tv/getEpg' - const hash = generateHash(data, endpoint) - - return `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}` - }, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - data({ date, channel }) { - const channelId = parseInt(channel.site_id) - - return { - locale: API.locale, - channelId: [channelId], - startTime: date.valueOf(), - endTime: date.add(1, 'd').valueOf(), - imageInfo: [{ height: 500, width: 1100 }], - includeBookmarks: false, - includeShow: true - } - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - category: parseCategory(item), - description: parseDescription(item), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = { - locale: API.locale, - type: 'TV', - imageInfo: [{ type: 'DARK', height: 70, width: 98 }] - } - const endpoint = 'client/channels/list' - const hash = generateHash(data, endpoint) - const response = await axios - .post( - `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}`, - data, - { - headers: { - 'Content-Type': 'application/json' - } - } - ) - .catch(console.log) - - return response.data.channels.map(item => { - return { - lang: 'sl', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseStart(item) { - return dayjs(parseInt(item.startTimestamp)) -} - -function parseStop(item) { - return dayjs(parseInt(item.endTimestamp)) -} - -function parseIcon(item) { - return item.images && item.images[0] ? `https://tv2go.t-2.net${item.images[0].url}` : null -} - -function parseCategory(item) { - return item.show && Array.isArray(item.show.genres) ? item.show.genres.map(c => c.name) : [] -} - -function parseDescription(item) { - return item.show ? item.show.shortDescription : null -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch (e) { - return [] - } - if (!data || !Array.isArray(data.entries)) return [] - - return data.entries -} - -function generateHash(data, endpoint) { - const salt = `${API.token}${API.version}${API.format}${API.uuid}` - - return md5(salt + endpoint + JSON.stringify(data)) -} - -module.exports = config +const axios = require('axios') +const dayjs = require('dayjs') +const md5 = require('./jquery.md5') + +const API = { + locale: 'sl-SI', + version: '9.4', + format: 'json', + uuid: '464830403846070', + token: '6dace810-55d5-11e3-949a-0800200c9a66' +} + +const config = { + site: 'tv2go.t-2.net', + days: 2, + url({ date, channel }) { + const data = config.request.data({ date, channel }) + const endpoint = 'client/tv/getEpg' + const hash = generateHash(data, endpoint) + + return `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}` + }, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + data({ date, channel }) { + const channelId = parseInt(channel.site_id) + + return { + locale: API.locale, + channelId: [channelId], + startTime: date.valueOf(), + endTime: date.add(1, 'd').valueOf(), + imageInfo: [{ height: 500, width: 1100 }], + includeBookmarks: false, + includeShow: true + } + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + category: parseCategory(item), + description: parseDescription(item), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = { + locale: API.locale, + type: 'TV', + imageInfo: [{ type: 'DARK', height: 70, width: 98 }] + } + const endpoint = 'client/channels/list' + const hash = generateHash(data, endpoint) + const response = await axios + .post( + `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}`, + data, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + .catch(console.log) + + return response.data.channels.map(item => { + return { + lang: 'sl', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseStart(item) { + return dayjs(parseInt(item.startTimestamp)) +} + +function parseStop(item) { + return dayjs(parseInt(item.endTimestamp)) +} + +function parseIcon(item) { + return item.images && item.images[0] ? `https://tv2go.t-2.net${item.images[0].url}` : null +} + +function parseCategory(item) { + return item.show && Array.isArray(item.show.genres) ? item.show.genres.map(c => c.name) : [] +} + +function parseDescription(item) { + return item.show ? item.show.shortDescription : null +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch (e) { + return [] + } + if (!data || !Array.isArray(data.entries)) return [] + + return data.entries +} + +function generateHash(data, endpoint) { + const salt = `${API.token}${API.version}${API.format}${API.uuid}` + + return md5(salt + endpoint + JSON.stringify(data)) +} + +module.exports = config diff --git a/sites/tv2go.t-2.net/tv2go.t-2.net.test.js b/sites/tv2go.t-2.net/tv2go.t-2.net.test.js index 2d69860d4..5adde2458 100644 --- a/sites/tv2go.t-2.net/tv2go.t-2.net.test.js +++ b/sites/tv2go.t-2.net/tv2go.t-2.net.test.js @@ -1,70 +1,70 @@ -// npm run channels:parse -- --config=./sites/tv2go.t-2.net/tv2go.t-2.net.config.js --output=./sites/tv2go.t-2.net/tv2go.t-2.net.channels.xml -// npm run grab -- --site=tv2go.t-2.net - -const { parser, url, request } = require('./tv2go.t-2.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1000259', - xmltv_id: 'TVSlovenija1.si' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://tv2go.t-2.net/Catherine/api/9.4/json/464830403846070/d79cf4dc84f2131689f426956b8d40de/client/tv/getEpg' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ date, channel })).toMatchObject({ - locale: 'sl-SI', - channelId: [1000259], - startTime: 1637280000000, - endTime: 1637366400000, - imageInfo: [{ height: 500, width: 1100 }], - includeBookmarks: false, - includeShow: true - }) -}) - -it('can parse response', () => { - const content = - '{"entries":[{"channelId":1000259,"startTimestamp":"1637283000000","endTimestamp":"1637284500000","name":"Dnevnik Slovencev v Italiji","nameSingleLine":"Dnevnik Slovencev v Italiji","description":"Informativni","images":[{"url":"/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg","width":1008,"height":720,"averageColor":[143,147,161]}],"show":{"id":51991133,"title":"Dnevnik Slovencev v Italiji","originalTitle":"Dnevnik Slovencev v Italiji","shortDescription":"Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.","longDescription":"Pomembno ogledalo vsakdana, v katerem opozarjajo na težave, s katerimi se soočajo, predstavljajo pa tudi pestro kulturno, športno in družbeno življenje slovenske narodne skupnosti. V oddajo so vključene tudi novice iz matične domovine.","type":{"id":10,"name":"Show"},"productionFrom":"1609502400000","countries":[{"id":"SI","name":"Slovenija"}],"languages":[{"languageId":2,"name":"Slovenščina"}],"genres":[{"id":1000002,"name":"Informativni"}]}}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-19T00:50:00.000Z', - stop: '2021-11-19T01:15:00.000Z', - title: 'Dnevnik Slovencev v Italiji', - category: ['Informativni'], - description: - 'Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.', - icon: 'https://tv2go.t-2.net/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: 'Invalid API client identifier' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv2go.t-2.net/tv2go.t-2.net.config.js --output=./sites/tv2go.t-2.net/tv2go.t-2.net.channels.xml +// npm run grab -- --site=tv2go.t-2.net + +const { parser, url, request } = require('./tv2go.t-2.net.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1000259', + xmltv_id: 'TVSlovenija1.si' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://tv2go.t-2.net/Catherine/api/9.4/json/464830403846070/d79cf4dc84f2131689f426956b8d40de/client/tv/getEpg' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ date, channel })).toMatchObject({ + locale: 'sl-SI', + channelId: [1000259], + startTime: 1637280000000, + endTime: 1637366400000, + imageInfo: [{ height: 500, width: 1100 }], + includeBookmarks: false, + includeShow: true + }) +}) + +it('can parse response', () => { + const content = + '{"entries":[{"channelId":1000259,"startTimestamp":"1637283000000","endTimestamp":"1637284500000","name":"Dnevnik Slovencev v Italiji","nameSingleLine":"Dnevnik Slovencev v Italiji","description":"Informativni","images":[{"url":"/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg","width":1008,"height":720,"averageColor":[143,147,161]}],"show":{"id":51991133,"title":"Dnevnik Slovencev v Italiji","originalTitle":"Dnevnik Slovencev v Italiji","shortDescription":"Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.","longDescription":"Pomembno ogledalo vsakdana, v katerem opozarjajo na težave, s katerimi se soočajo, predstavljajo pa tudi pestro kulturno, športno in družbeno življenje slovenske narodne skupnosti. V oddajo so vključene tudi novice iz matične domovine.","type":{"id":10,"name":"Show"},"productionFrom":"1609502400000","countries":[{"id":"SI","name":"Slovenija"}],"languages":[{"languageId":2,"name":"Slovenščina"}],"genres":[{"id":1000002,"name":"Informativni"}]}}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-19T00:50:00.000Z', + stop: '2021-11-19T01:15:00.000Z', + title: 'Dnevnik Slovencev v Italiji', + category: ['Informativni'], + description: + 'Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.', + icon: 'https://tv2go.t-2.net/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: 'Invalid API client identifier' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tva.tv/tva.tv.config.js b/sites/tva.tv/tva.tv.config.js index f2a6a6704..41b9a7564 100644 --- a/sites/tva.tv/tva.tv.config.js +++ b/sites/tva.tv/tva.tv.config.js @@ -1,58 +1,58 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tva.tv', - days: 2, - url: function ({ date, channel }) { - return `https://api.ott.tva.tv/v2/epg/program_events.json?channel_id=${ - channel.site_id - }&pivot_date=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.description, - start: dayjs(item.start_at), - stop: dayjs(item.end_at) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get( - 'https://api.ott.tva.tv/v1/channels?client_id=66797942-ff54-46cb-a109-3bae7c855370&client_version=0.0.1&expand%5Bchannel%5D=images&locale=en-GB&page%5Blimit%5D=100&page%5Boffset%5D=0&timezone=10800', - { - headers: { - Origin: 'https://tva.tv' - } - } - ) - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let item of data.data) { - channels.push({ - lang: 'fa', - site_id: item.id, - name: item.name, - xmltv_id: item.slug - }) - } - - return channels - } -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data)) return [] - - return data.data -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tva.tv', + days: 2, + url: function ({ date, channel }) { + return `https://api.ott.tva.tv/v2/epg/program_events.json?channel_id=${ + channel.site_id + }&pivot_date=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.description, + start: dayjs(item.start_at), + stop: dayjs(item.end_at) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get( + 'https://api.ott.tva.tv/v1/channels?client_id=66797942-ff54-46cb-a109-3bae7c855370&client_version=0.0.1&expand%5Bchannel%5D=images&locale=en-GB&page%5Blimit%5D=100&page%5Boffset%5D=0&timezone=10800', + { + headers: { + Origin: 'https://tva.tv' + } + } + ) + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let item of data.data) { + channels.push({ + lang: 'fa', + site_id: item.id, + name: item.name, + xmltv_id: item.slug + }) + } + + return channels + } +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data)) return [] + + return data.data +} diff --git a/sites/tva.tv/tva.tv.test.js b/sites/tva.tv/tva.tv.test.js index bd9504f11..6d6d1fa24 100644 --- a/sites/tva.tv/tva.tv.test.js +++ b/sites/tva.tv/tva.tv.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/tva.tv/tva.tv.config.js --output=./sites/tva.tv/tva.tv.channels.xml -// npm run grab -- --site=tva.tv - -const { parser, url } = require('./tva.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '6fcc0a2e-1135-482c-b054-08a96e68b758', - xmltv_id: 'IRIB2.ir' -} -const content = - '{"data":[{"id":"c0667229-eaf8-472f-8ba7-ad4417348baf","start_at":"2021-11-24T00:20:39.000Z","end_at":"2021-11-24T00:32:11.000Z","description":"تلفن های شبکه 5 سیما: تلفن: 23511000 -تلفن گویا:2786500 نمابر:23511289","name":"میان برنامه","subtitle":"","season_number":null,"episode_number":null,"channel_id":"6fcc0a2e-1135-482c-b054-08a96e68b758","program_id":"e495c06e-80de-46ee-9120-619631f554d9","competition_id":null,"object":"program_event","cast_members":[],"genres":[],"images":[],"program_type":null,"certification_ratings":[]}]}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.ott.tva.tv/v2/epg/program_events.json?channel_id=6fcc0a2e-1135-482c-b054-08a96e68b758&pivot_date=2021-11-25' - ) -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T00:20:39.000Z', - stop: '2021-11-24T00:32:11.000Z', - title: 'میان برنامه', - description: 'تلفن های شبکه 5 سیما: تلفن: 23511000 -تلفن گویا:2786500 نمابر:23511289' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"errors":[{"id":"ebbebfb7-ffb0-4e0b-bcfb-1d9cd3e6c03c","code":"not_found","links":{"about":{"href":"https://app.bugsnag.com/jeytv/API/errors?filters[event.since][]=30d&filters[user.name][]=ebbebfb7-ffb0-4e0b-bcfb-1d9cd3e6c03c"}},"title":"Requested resource was not found","fallback_message":null,"object":"error"}],"meta":{"status":404}}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tva.tv/tva.tv.config.js --output=./sites/tva.tv/tva.tv.channels.xml +// npm run grab -- --site=tva.tv + +const { parser, url } = require('./tva.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '6fcc0a2e-1135-482c-b054-08a96e68b758', + xmltv_id: 'IRIB2.ir' +} +const content = + '{"data":[{"id":"c0667229-eaf8-472f-8ba7-ad4417348baf","start_at":"2021-11-24T00:20:39.000Z","end_at":"2021-11-24T00:32:11.000Z","description":"تلفن های شبکه 5 سیما: تلفن: 23511000 -تلفن گویا:2786500 نمابر:23511289","name":"میان برنامه","subtitle":"","season_number":null,"episode_number":null,"channel_id":"6fcc0a2e-1135-482c-b054-08a96e68b758","program_id":"e495c06e-80de-46ee-9120-619631f554d9","competition_id":null,"object":"program_event","cast_members":[],"genres":[],"images":[],"program_type":null,"certification_ratings":[]}]}' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.ott.tva.tv/v2/epg/program_events.json?channel_id=6fcc0a2e-1135-482c-b054-08a96e68b758&pivot_date=2021-11-25' + ) +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T00:20:39.000Z', + stop: '2021-11-24T00:32:11.000Z', + title: 'میان برنامه', + description: 'تلفن های شبکه 5 سیما: تلفن: 23511000 -تلفن گویا:2786500 نمابر:23511289' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"errors":[{"id":"ebbebfb7-ffb0-4e0b-bcfb-1d9cd3e6c03c","code":"not_found","links":{"about":{"href":"https://app.bugsnag.com/jeytv/API/errors?filters[event.since][]=30d&filters[user.name][]=ebbebfb7-ffb0-4e0b-bcfb-1d9cd3e6c03c"}},"title":"Requested resource was not found","fallback_message":null,"object":"error"}],"meta":{"status":404}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvarenasport.com/tvarenasport.com.config.js b/sites/tvarenasport.com/tvarenasport.com.config.js index 53d91cf16..d7b0e3139 100644 --- a/sites/tvarenasport.com/tvarenasport.com.config.js +++ b/sites/tvarenasport.com/tvarenasport.com.config.js @@ -1,50 +1,50 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvarenasport.com', - days: 2, - url: function ({ date }) { - return `https://www.tvarenasport.com/api/schedule?date=${date.format('DD-MM-YYYY')}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title.trim(), - category: item.league, - description: item.sport.trim(), - start: dayjs(item.start), - stop: dayjs(item.end) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.tvarenasport.com/api/schedule') - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let id in data.channels) { - const item = data.channels[id] - channels.push({ - lang: 'sr', - site_id: id, - name: item.name - }) - } - - return channels - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.items)) return [] - - return data.items.filter(i => i.group === channel.site_id) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvarenasport.com', + days: 2, + url: function ({ date }) { + return `https://www.tvarenasport.com/api/schedule?date=${date.format('DD-MM-YYYY')}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title.trim(), + category: item.league, + description: item.sport.trim(), + start: dayjs(item.start), + stop: dayjs(item.end) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.tvarenasport.com/api/schedule') + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let id in data.channels) { + const item = data.channels[id] + channels.push({ + lang: 'sr', + site_id: id, + name: item.name + }) + } + + return channels + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.items)) return [] + + return data.items.filter(i => i.group === channel.site_id) +} diff --git a/sites/tvarenasport.com/tvarenasport.com.test.js b/sites/tvarenasport.com/tvarenasport.com.test.js index 9460a3a6b..4781a9eac 100644 --- a/sites/tvarenasport.com/tvarenasport.com.test.js +++ b/sites/tvarenasport.com/tvarenasport.com.test.js @@ -1,48 +1,48 @@ -// npm run channels:parse -- --config=./sites/tvarenasport.com/tvarenasport.com.config.js --output=./sites/tvarenasport.com/tvarenasport.com.channels.xml --set=country:rs -// npm run grab -- --site=tvarenasport.com - -const { parser, url } = require('./tvarenasport.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '380', - xmltv_id: 'ArenaSport1.rs' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvarenasport.com/api/schedule?date=17-11-2021') -}) - -it('can parse response', () => { - const content = - '{"items":[{"id":2857,"title":"Crvena zvezda mts - Partizan NIS","start":"2021-11-16T23:30:00Z","end":"2021-11-17T01:30:00Z","sport":"ABA LIGA","league":"Ko\u0161arka","group":"380","isLive":false,"doNotMiss":false,"domain":"srb"},{"id":3155,"title":"Sao Paulo - Flamengo","start":"2021-11-17T00:00:00Z","end":"2021-11-17T02:00:00Z","sport":"BRAZILSKA LIGA","league":"Fudbal","group":"381","isLive":false,"doNotMiss":false,"domain":"srb"}]}' - const result = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T23:30:00.000Z', - stop: '2021-11-17T01:30:00.000Z', - title: 'Crvena zvezda mts - Partizan NIS', - category: 'Ko\u0161arka', - description: 'ABA LIGA' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvarenasport.com/tvarenasport.com.config.js --output=./sites/tvarenasport.com/tvarenasport.com.channels.xml --set=country:rs +// npm run grab -- --site=tvarenasport.com + +const { parser, url } = require('./tvarenasport.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '380', + xmltv_id: 'ArenaSport1.rs' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvarenasport.com/api/schedule?date=17-11-2021') +}) + +it('can parse response', () => { + const content = + '{"items":[{"id":2857,"title":"Crvena zvezda mts - Partizan NIS","start":"2021-11-16T23:30:00Z","end":"2021-11-17T01:30:00Z","sport":"ABA LIGA","league":"Ko\u0161arka","group":"380","isLive":false,"doNotMiss":false,"domain":"srb"},{"id":3155,"title":"Sao Paulo - Flamengo","start":"2021-11-17T00:00:00Z","end":"2021-11-17T02:00:00Z","sport":"BRAZILSKA LIGA","league":"Fudbal","group":"381","isLive":false,"doNotMiss":false,"domain":"srb"}]}' + const result = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T23:30:00.000Z', + stop: '2021-11-17T01:30:00.000Z', + title: 'Crvena zvezda mts - Partizan NIS', + category: 'Ko\u0161arka', + description: 'ABA LIGA' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"channels":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvarenasport.hr/tvarenasport.hr.config.js b/sites/tvarenasport.hr/tvarenasport.hr.config.js index 518d97c4a..a6ad4706f 100644 --- a/sites/tvarenasport.hr/tvarenasport.hr.config.js +++ b/sites/tvarenasport.hr/tvarenasport.hr.config.js @@ -1,50 +1,50 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvarenasport.hr', - days: 2, - url: function ({ date }) { - return `https://www.tvarenasport.hr/api/schedule?date=${date.format('DD-MM-YYYY')}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title.trim(), - category: item.sport, - description: item.league.trim(), - start: dayjs(item.start), - stop: dayjs(item.end) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.tvarenasport.hr/api/schedule') - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let id in data.channels) { - const item = data.channels[id] - channels.push({ - lang: 'hr', - site_id: id, - name: item.name - }) - } - - return channels - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.items)) return [] - - return data.items.filter(i => i.group === channel.site_id) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvarenasport.hr', + days: 2, + url: function ({ date }) { + return `https://www.tvarenasport.hr/api/schedule?date=${date.format('DD-MM-YYYY')}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title.trim(), + category: item.sport, + description: item.league.trim(), + start: dayjs(item.start), + stop: dayjs(item.end) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.tvarenasport.hr/api/schedule') + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let id in data.channels) { + const item = data.channels[id] + channels.push({ + lang: 'hr', + site_id: id, + name: item.name + }) + } + + return channels + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.items)) return [] + + return data.items.filter(i => i.group === channel.site_id) +} diff --git a/sites/tvarenasport.hr/tvarenasport.hr.test.js b/sites/tvarenasport.hr/tvarenasport.hr.test.js index 30cf22e2c..966e6220f 100644 --- a/sites/tvarenasport.hr/tvarenasport.hr.test.js +++ b/sites/tvarenasport.hr/tvarenasport.hr.test.js @@ -1,48 +1,48 @@ -// npm run channels:parse -- --config=./sites/tvarenasport.hr/tvarenasport.hr.config.js --output=./sites/tvarenasport.hr/tvarenasport.hr.channels.xml -// npm run grab -- --site=tvarenasport.hr - -const { parser, url } = require('./tvarenasport.hr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '380', - xmltv_id: 'ArenaSport1Croatia.hr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvarenasport.hr/api/schedule?date=17-11-2021') -}) - -it('can parse response', () => { - const content = - '{"items":[{"id":6104,"title":"NAJAVA PROGRAMA","start":"2021-11-16T23:00:00Z","end":"2021-11-17T23:00:00Z","sport":"Najava programa","league":"NAJAVA PROGRAMA","group":"1294","isLive":false,"doNotMiss":false,"domain":"cro"},{"id":6000,"title":" DIJON - UNICAJA","start":"2021-11-16T23:30:00Z","end":"2021-11-17T01:00:00Z","sport":"Košarka","league":" LIGA PRVAKA","group":"380","isLive":false,"doNotMiss":false,"domain":"cro"}]}' - const result = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T23:30:00.000Z', - stop: '2021-11-17T01:00:00.000Z', - title: 'DIJON - UNICAJA', - category: 'Košarka', - description: 'LIGA PRVAKA' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvarenasport.hr/tvarenasport.hr.config.js --output=./sites/tvarenasport.hr/tvarenasport.hr.channels.xml +// npm run grab -- --site=tvarenasport.hr + +const { parser, url } = require('./tvarenasport.hr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '380', + xmltv_id: 'ArenaSport1Croatia.hr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvarenasport.hr/api/schedule?date=17-11-2021') +}) + +it('can parse response', () => { + const content = + '{"items":[{"id":6104,"title":"NAJAVA PROGRAMA","start":"2021-11-16T23:00:00Z","end":"2021-11-17T23:00:00Z","sport":"Najava programa","league":"NAJAVA PROGRAMA","group":"1294","isLive":false,"doNotMiss":false,"domain":"cro"},{"id":6000,"title":" DIJON - UNICAJA","start":"2021-11-16T23:30:00Z","end":"2021-11-17T01:00:00Z","sport":"Košarka","league":" LIGA PRVAKA","group":"380","isLive":false,"doNotMiss":false,"domain":"cro"}]}' + const result = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T23:30:00.000Z', + stop: '2021-11-17T01:00:00.000Z', + title: 'DIJON - UNICAJA', + category: 'Košarka', + description: 'LIGA PRVAKA' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"channels":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js index 8aecfdfd4..515da300b 100644 --- a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js +++ b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(timezone) - -module.exports = { - site: 'tvcubana.icrt.cu', - days: 2, - url({ channel, date }) { - const daysOfWeek = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado'] - - return `https://www.tvcubana.icrt.cu/cartv/${channel.site_id}/${daysOfWeek[date.day()]}.php` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.tz(item.eventInitialDateTime, 'America/Havana') -} - -function parseStop(item) { - return dayjs.tz(item.eventEndDateTime, 'America/Havana') -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch (error) { - return [] - } - if (!data || !Array.isArray(data)) return [] - - return data -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(timezone) + +module.exports = { + site: 'tvcubana.icrt.cu', + days: 2, + url({ channel, date }) { + const daysOfWeek = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado'] + + return `https://www.tvcubana.icrt.cu/cartv/${channel.site_id}/${daysOfWeek[date.day()]}.php` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.tz(item.eventInitialDateTime, 'America/Havana') +} + +function parseStop(item) { + return dayjs.tz(item.eventEndDateTime, 'America/Havana') +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch (error) { + return [] + } + if (!data || !Array.isArray(data)) return [] + + return data +} diff --git a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js index d04759db8..4789b0d57 100644 --- a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js +++ b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=tvcubana.icrt.cu - -const { parser, url } = require('./tvcubana.icrt.cu.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'cv', - xmltv_id: 'CubavisionNacional.cu' -} -const content = - '[{"eventId":"6169c2300ad38b0a8d9e3760","title":"CARIBE NOTICIAS","description":"EMISI\\u00d3N DE CIERRE.","eventInitialDate":"2021-11-22T00:00:00","eventEndDate":"2021-11-22T00:00:00","idFromEprog":"5c096ea5bad1b202541503cf","extendedDescription":"","transmission":"Estreno","pid":"","space":"CARIBE NOTICIAS","eventStartTime":{"value":{"ticks":24000000000,"days":0,"hours":0,"milliseconds":0,"minutes":40,"seconds":0,"totalDays":0.027777777777777776,"totalHours":0.6666666666666666,"totalMilliseconds":2400000,"totalMinutes":40,"totalSeconds":2400},"hasValue":true},"eventEndTime":{"value":{"ticks":30000000000,"days":0,"hours":0,"milliseconds":0,"minutes":50,"seconds":0,"totalDays":0.034722222222222224,"totalHours":0.8333333333333334,"totalMilliseconds":3000000,"totalMinutes":50,"totalSeconds":3000},"hasValue":true},"eventDuration":"00:10:00","channelName":"Cubavisi\\u00f3n","eventInitialDateTime":"2021-11-22T00:40:00","eventEndDateTime":"2021-11-22T00:50:00","isEventWithNegativeDuration":false,"isEventWithDurationOver24Hrs":false,"isEventWithTextOverLength":false,"created":"2021-11-22T10:32:27.476824","id":5309687}]' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvcubana.icrt.cu/cartv/cv/lunes.php') -}) - -it('can generate valid url for next day', () => { - expect(url({ channel, date: date.add(2, 'd') })).toBe( - 'https://www.tvcubana.icrt.cu/cartv/cv/miercoles.php' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-22T05:40:00.000Z', - stop: '2021-11-22T05:50:00.000Z', - title: 'CARIBE NOTICIAS', - description: 'EMISIÓN DE CIERRE.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvcubana.icrt.cu + +const { parser, url } = require('./tvcubana.icrt.cu.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'cv', + xmltv_id: 'CubavisionNacional.cu' +} +const content = + '[{"eventId":"6169c2300ad38b0a8d9e3760","title":"CARIBE NOTICIAS","description":"EMISI\\u00d3N DE CIERRE.","eventInitialDate":"2021-11-22T00:00:00","eventEndDate":"2021-11-22T00:00:00","idFromEprog":"5c096ea5bad1b202541503cf","extendedDescription":"","transmission":"Estreno","pid":"","space":"CARIBE NOTICIAS","eventStartTime":{"value":{"ticks":24000000000,"days":0,"hours":0,"milliseconds":0,"minutes":40,"seconds":0,"totalDays":0.027777777777777776,"totalHours":0.6666666666666666,"totalMilliseconds":2400000,"totalMinutes":40,"totalSeconds":2400},"hasValue":true},"eventEndTime":{"value":{"ticks":30000000000,"days":0,"hours":0,"milliseconds":0,"minutes":50,"seconds":0,"totalDays":0.034722222222222224,"totalHours":0.8333333333333334,"totalMilliseconds":3000000,"totalMinutes":50,"totalSeconds":3000},"hasValue":true},"eventDuration":"00:10:00","channelName":"Cubavisi\\u00f3n","eventInitialDateTime":"2021-11-22T00:40:00","eventEndDateTime":"2021-11-22T00:50:00","isEventWithNegativeDuration":false,"isEventWithDurationOver24Hrs":false,"isEventWithTextOverLength":false,"created":"2021-11-22T10:32:27.476824","id":5309687}]' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvcubana.icrt.cu/cartv/cv/lunes.php') +}) + +it('can generate valid url for next day', () => { + expect(url({ channel, date: date.add(2, 'd') })).toBe( + 'https://www.tvcubana.icrt.cu/cartv/cv/miercoles.php' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-22T05:40:00.000Z', + stop: '2021-11-22T05:50:00.000Z', + title: 'CARIBE NOTICIAS', + description: 'EMISIÓN DE CIERRE.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvgids.nl/tvgids.nl.config.js b/sites/tvgids.nl/tvgids.nl.config.js index 8c2e13f49..b05243581 100644 --- a/sites/tvgids.nl/tvgids.nl.config.js +++ b/sites/tvgids.nl/tvgids.nl.config.js @@ -1,87 +1,87 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tvgids.nl', - days: 2, - url: function ({ date, channel }) { - const path = - DateTime.now().day === DateTime.fromMillis(date.valueOf()).day - ? '' - : `${date.format('DD-MM-YYYY')}/` - - return `https://www.tvgids.nl/gids/${path}${channel.site_id}` - }, - parser: function ({ content, date }) { - date = date.subtract(1, 'd') - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.tvgids.nl/gids/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - - const channels = [] - $('#channel-container > div').each((i, el) => { - channels.push({ - site_id: $(el).find('a').attr('id'), - name: $(el).find('img').attr('title'), - lang: 'nl' - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.program__title').text().trim() -} - -function parseDescription($item) { - return $item('.program__text').text().trim() -} - -function parseIcon($item) { - return $item('.program__thumbnail').data('src') -} - -function parseStart($item, date) { - const time = $item('.program__starttime').clone().children().remove().end().text().trim() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Amsterdam' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.guide__guide .program').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tvgids.nl', + days: 2, + url: function ({ date, channel }) { + const path = + DateTime.now().day === DateTime.fromMillis(date.valueOf()).day + ? '' + : `${date.format('DD-MM-YYYY')}/` + + return `https://www.tvgids.nl/gids/${path}${channel.site_id}` + }, + parser: function ({ content, date }) { + date = date.subtract(1, 'd') + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.tvgids.nl/gids/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + + const channels = [] + $('#channel-container > div').each((i, el) => { + channels.push({ + site_id: $(el).find('a').attr('id'), + name: $(el).find('img').attr('title'), + lang: 'nl' + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.program__title').text().trim() +} + +function parseDescription($item) { + return $item('.program__text').text().trim() +} + +function parseIcon($item) { + return $item('.program__thumbnail').data('src') +} + +function parseStart($item, date) { + const time = $item('.program__starttime').clone().children().remove().end().text().trim() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Amsterdam' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.guide__guide .program').toArray() +} diff --git a/sites/tvgids.nl/tvgids.nl.test.js b/sites/tvgids.nl/tvgids.nl.test.js index d30af52b3..839976acb 100644 --- a/sites/tvgids.nl/tvgids.nl.test.js +++ b/sites/tvgids.nl/tvgids.nl.test.js @@ -1,61 +1,61 @@ -// npm run channels:parse -- --config=./sites/tvgids.nl/tvgids.nl.config.js --output=./sites/tvgids.nl/tvgids.nl.channels.xml -// npm run grab -- --site=tvgids.nl - -const { parser, url } = require('./tvgids.nl.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'npo1', - xmltv_id: 'NPO1.nl' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://www.tvgids.nl/gids/13-06-2023/npo1') -}) - -it('can generate valid url for today', () => { - const today = dayjs().startOf('d') - - expect(url({ date: today, channel })).toBe('https://www.tvgids.nl/gids/npo1') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-12T21:57:00.000Z', - stop: '2023-06-12T22:58:00.000Z', - title: 'Op1', - icon: 'https://tvgidsassets.nl/v301/upload/o/carrousel/op1-451542641.jpg', - description: "Talkshow met wisselende presentatieduo's, live vanuit Amsterdam." - }) - - expect(results[61]).toMatchObject({ - start: '2023-06-14T00:18:00.000Z', - stop: '2023-06-14T00:48:00.000Z', - title: 'NOS Journaal', - icon: 'https://tvgidsassets.nl/v301/upload/n/carrousel/nos-journaal-452818771.jpg', - description: - 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor vandaag.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - date - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvgids.nl/tvgids.nl.config.js --output=./sites/tvgids.nl/tvgids.nl.channels.xml +// npm run grab -- --site=tvgids.nl + +const { parser, url } = require('./tvgids.nl.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'npo1', + xmltv_id: 'NPO1.nl' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://www.tvgids.nl/gids/13-06-2023/npo1') +}) + +it('can generate valid url for today', () => { + const today = dayjs().startOf('d') + + expect(url({ date: today, channel })).toBe('https://www.tvgids.nl/gids/npo1') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-12T21:57:00.000Z', + stop: '2023-06-12T22:58:00.000Z', + title: 'Op1', + icon: 'https://tvgidsassets.nl/v301/upload/o/carrousel/op1-451542641.jpg', + description: "Talkshow met wisselende presentatieduo's, live vanuit Amsterdam." + }) + + expect(results[61]).toMatchObject({ + start: '2023-06-14T00:18:00.000Z', + stop: '2023-06-14T00:48:00.000Z', + title: 'NOS Journaal', + icon: 'https://tvgidsassets.nl/v301/upload/n/carrousel/nos-journaal-452818771.jpg', + description: + 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor vandaag.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvguide.com/tvguide.com.config.js b/sites/tvguide.com/tvguide.com.config.js index c9a1f8e77..e08b30952 100644 --- a/sites/tvguide.com/tvguide.com.config.js +++ b/sites/tvguide.com/tvguide.com.config.js @@ -1,71 +1,71 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'tvguide.com', - days: 2, - url: function ({ date, channel }) { - const [providerId, channelSourceIds] = channel.site_id.split('#') - const url = `https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/${providerId}/web?start=${date - .startOf('d') - .unix()}&duration=1440&channelSourceIds=${channelSourceIds}` - - return url - }, - async parser({ content }) { - const programs = [] - const items = parseItems(content) - for (let item of items) { - const details = await loadProgramDetails(item) - programs.push({ - title: item.title, - sub_title: details.episodeTitle, - description: details.description, - season: details.seasonNumber, - episode: details.episodeNumber, - rating: parseRating(item), - categories: parseCategories(details), - start: parseTime(item.startTime), - stop: parseTime(item.endTime) - }) - } - - return programs - } -} - -function parseRating(item) { - return item.rating ? { system: 'MPA', value: item.rating } : null -} - -function parseCategories(details) { - return Array.isArray(details.genres) ? details.genres.map(g => g.name) : [] -} - -function parseTime(timestamp) { - return dayjs.unix(timestamp) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data.data || !Array.isArray(data.data.items) || !data.data.items.length) return [] - - return data.data.items[0].programSchedules -} - -async function loadProgramDetails(item) { - const data = await axios - .get(item.programDetails) - .then(r => r.data) - .catch(err => { - console.log(err.message) - }) - if (!data || !data.data || !data.data.item) return {} - - return data.data.item -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'tvguide.com', + days: 2, + url: function ({ date, channel }) { + const [providerId, channelSourceIds] = channel.site_id.split('#') + const url = `https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/${providerId}/web?start=${date + .startOf('d') + .unix()}&duration=1440&channelSourceIds=${channelSourceIds}` + + return url + }, + async parser({ content }) { + const programs = [] + const items = parseItems(content) + for (let item of items) { + const details = await loadProgramDetails(item) + programs.push({ + title: item.title, + sub_title: details.episodeTitle, + description: details.description, + season: details.seasonNumber, + episode: details.episodeNumber, + rating: parseRating(item), + categories: parseCategories(details), + start: parseTime(item.startTime), + stop: parseTime(item.endTime) + }) + } + + return programs + } +} + +function parseRating(item) { + return item.rating ? { system: 'MPA', value: item.rating } : null +} + +function parseCategories(details) { + return Array.isArray(details.genres) ? details.genres.map(g => g.name) : [] +} + +function parseTime(timestamp) { + return dayjs.unix(timestamp) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data.data || !Array.isArray(data.data.items) || !data.data.items.length) return [] + + return data.data.items[0].programSchedules +} + +async function loadProgramDetails(item) { + const data = await axios + .get(item.programDetails) + .then(r => r.data) + .catch(err => { + console.log(err.message) + }) + if (!data || !data.data || !data.data.item) return {} + + return data.data.item +} diff --git a/sites/tvguide.com/tvguide.com.test.js b/sites/tvguide.com/tvguide.com.test.js index 3010d454f..075385916 100644 --- a/sites/tvguide.com/tvguide.com.test.js +++ b/sites/tvguide.com/tvguide.com.test.js @@ -1,74 +1,74 @@ -// npm run grab -- --site=tvguide.com - -const { parser, url } = require('./tvguide.com.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '9100001138#9200018514', - xmltv_id: 'CBSEast.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/9100001138/web?start=1667088000&duration=1440&channelSourceIds=9200018514' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/programdetails/6060613824/web' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[5]).toMatchObject({ - start: '2022-10-30T13:00:00.000Z', - stop: '2022-10-30T14:30:00.000Z', - title: 'CBS Sunday Morning', - sub_title: '10-30-2022', - description: - 'The Backseat Lovers perform on the "Saturday Sessions"; and Daisy Ryan guests on "The Dish." Also: comedian Fortune Feimster.', - categories: ['Talk & Interview', 'Other'], - season: 40, - episode: 248, - rating: { - system: 'MPA', - value: 'TV-PG' - } - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) - }) - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=tvguide.com + +const { parser, url } = require('./tvguide.com.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '9100001138#9200018514', + xmltv_id: 'CBSEast.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/9100001138/web?start=1667088000&duration=1440&channelSourceIds=9200018514' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/programdetails/6060613824/web' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[5]).toMatchObject({ + start: '2022-10-30T13:00:00.000Z', + stop: '2022-10-30T14:30:00.000Z', + title: 'CBS Sunday Morning', + sub_title: '10-30-2022', + description: + 'The Backseat Lovers perform on the "Saturday Sessions"; and Daisy Ryan guests on "The Dish." Also: comedian Fortune Feimster.', + categories: ['Talk & Interview', 'Other'], + season: 40, + episode: 248, + rating: { + system: 'MPA', + value: 'TV-PG' + } + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js index adbb0c13f..627b997ca 100644 --- a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js +++ b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js @@ -1,113 +1,113 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvguide.myjcom.jp', - days: 2, - url: function ({ date, channel }) { - const id = `${channel.site_id}_${date.format('YYYYMMDD')}` - - return `https://tvguide.myjcom.jp/api/getEpgInfo/?channels=${id}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.commentary, - category: parseCategory(item), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const requests = [ - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=2&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=3&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=5&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=120&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=200&area=108&channelGenre&course&chart&is_adult=true' - ) - ] - - let items = [] - await Promise.all(requests) - .then(responses => { - for (const r of responses) { - items = items.concat(r.data.header) - } - }) - .catch(console.log) - - return items.map(item => { - return { - lang: 'jp', - site_id: `${item.channel_type}_${item.channel_id}_${item.network_id}`, - name: item.channel_name - } - }) - } -} - -function parseIcon(item) { - return item.imgPath ? `https://tvguide.myjcom.jp${item.imgPath}` : null -} - -function parseCategory(item) { - if (!item.sortGenre) return null - - const id = item.sortGenre[0] - const genres = { - 0: 'ニュース/報道', - 1: 'スポーツ', - 2: '情報/ワイドショー', - 3: 'ドラマ', - 4: '音楽', - 5: 'バラエティ', - 6: '映画', - 7: 'アニメ/特撮', - 8: 'ドキュメンタリー/教養', - 9: '劇場/公演', - 10: '趣味/教育', - 11: '福祉', - 12: 'その他' - } - - return genres[id] -} - -function parseStart(item) { - return dayjs.tz(item.programStart.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') -} - -function parseStop(item) { - return dayjs.tz(item.programEnd.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') -} - -function parseItems(content, channel, date) { - const id = `${channel.site_id}_${date.format('YYYYMMDD')}` - const parsed = JSON.parse(content) - - return parsed[id] || [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvguide.myjcom.jp', + days: 2, + url: function ({ date, channel }) { + const id = `${channel.site_id}_${date.format('YYYYMMDD')}` + + return `https://tvguide.myjcom.jp/api/getEpgInfo/?channels=${id}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.commentary, + category: parseCategory(item), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const requests = [ + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=2&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=3&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=5&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=120&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=200&area=108&channelGenre&course&chart&is_adult=true' + ) + ] + + let items = [] + await Promise.all(requests) + .then(responses => { + for (const r of responses) { + items = items.concat(r.data.header) + } + }) + .catch(console.log) + + return items.map(item => { + return { + lang: 'jp', + site_id: `${item.channel_type}_${item.channel_id}_${item.network_id}`, + name: item.channel_name + } + }) + } +} + +function parseIcon(item) { + return item.imgPath ? `https://tvguide.myjcom.jp${item.imgPath}` : null +} + +function parseCategory(item) { + if (!item.sortGenre) return null + + const id = item.sortGenre[0] + const genres = { + 0: 'ニュース/報道', + 1: 'スポーツ', + 2: '情報/ワイドショー', + 3: 'ドラマ', + 4: '音楽', + 5: 'バラエティ', + 6: '映画', + 7: 'アニメ/特撮', + 8: 'ドキュメンタリー/教養', + 9: '劇場/公演', + 10: '趣味/教育', + 11: '福祉', + 12: 'その他' + } + + return genres[id] +} + +function parseStart(item) { + return dayjs.tz(item.programStart.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') +} + +function parseStop(item) { + return dayjs.tz(item.programEnd.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') +} + +function parseItems(content, channel, date) { + const id = `${channel.site_id}_${date.format('YYYYMMDD')}` + const parsed = JSON.parse(content) + + return parsed[id] || [] +} diff --git a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js index 3f3b7c354..520d7a3ac 100644 --- a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js +++ b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js @@ -1,52 +1,52 @@ -// npm run channels:parse -- --config=./sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js --output=./sites/tvguide.myjcom.jp/tvguide.myjcom.jp.channels.xml -// npm run grab -- --site=tvguide.myjcom.jp - -const { parser, url } = require('./tvguide.myjcom.jp.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-01-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '120_200_4', - name: 'Star Channel 1', - xmltv_id: 'StarChannel1.jp' -} -const content = - '{"120_200_4_20220114":[{"@search.score":1,"cid":"120_7305523","serviceCode":"200_4","channelName":"スターチャンネル1","digitalNo":195,"eventId":"181","title":"[5.1]フードロア:タマリンド","commentary":"HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)","attr":["5.1","hd","cp1"],"sortGenre":"31","hasImage":1,"imgPath":"/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg","isRecommended":null,"programStart":20220114050000,"programEnd":20220114060000,"programDate":20220114,"programId":568519,"start_time":"00","duration":60,"top":300,"end_time":"20220114060000","channel_type":"120","is_end":false,"show_remoterec":true}]}' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://tvguide.myjcom.jp/api/getEpgInfo/?channels=120_200_4_20220114') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-13T20:00:00.000Z', - stop: '2022-01-13T21:00:00.000Z', - title: '[5.1]フードロア:タマリンド', - description: - 'HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)', - icon: 'https://tvguide.myjcom.jp/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg', - category: 'ドラマ' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"120_200_3_20220114":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js --output=./sites/tvguide.myjcom.jp/tvguide.myjcom.jp.channels.xml +// npm run grab -- --site=tvguide.myjcom.jp + +const { parser, url } = require('./tvguide.myjcom.jp.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '120_200_4', + name: 'Star Channel 1', + xmltv_id: 'StarChannel1.jp' +} +const content = + '{"120_200_4_20220114":[{"@search.score":1,"cid":"120_7305523","serviceCode":"200_4","channelName":"スターチャンネル1","digitalNo":195,"eventId":"181","title":"[5.1]フードロア:タマリンド","commentary":"HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)","attr":["5.1","hd","cp1"],"sortGenre":"31","hasImage":1,"imgPath":"/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg","isRecommended":null,"programStart":20220114050000,"programEnd":20220114060000,"programDate":20220114,"programId":568519,"start_time":"00","duration":60,"top":300,"end_time":"20220114060000","channel_type":"120","is_end":false,"show_remoterec":true}]}' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://tvguide.myjcom.jp/api/getEpgInfo/?channels=120_200_4_20220114') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-13T20:00:00.000Z', + stop: '2022-01-13T21:00:00.000Z', + title: '[5.1]フードロア:タマリンド', + description: + 'HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)', + icon: 'https://tvguide.myjcom.jp/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg', + category: 'ドラマ' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"120_200_3_20220114":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvhebdo.com/tvhebdo.com.config.js b/sites/tvhebdo.com/tvhebdo.com.config.js index 28932384e..17bb5586c 100644 --- a/sites/tvhebdo.com/tvhebdo.com.config.js +++ b/sites/tvhebdo.com/tvhebdo.com.config.js @@ -1,94 +1,94 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tvhebdo.com', - days: 2, - url: function ({ channel, date }) { - return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - } - prev.stop = start - } - let stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let items = [] - const offsets = [ - 0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360 - ] - for (let offset of offsets) { - const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00` - console.log(url) - const html = await axios - .get(url, { - headers: { - Cookie: - 'distributeur=8004264; __utmz=222163677.1652094266.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _gcl_au=1.1.656635701.1652094273; tvh=3c2kaml9u14m83v91bg4dqgaf3; __utmc=222163677; IR_gbd=tvhebdo.com; IR_MPI=cf76b363-cf87-11ec-93f5-13daf79f8f76%7C1652367602625; __utma=222163677.2064368965.1652094266.1652281202.1652281479.3; __utmt=1; IR_MPS=1652284935955%7C1652284314367; _uetsid=0d8e2e60d13b11ec850db551304ae9e7; _uetvid=80456fa0b26e11ec9bf94951ce79b5f8; __utmb=222163677.19.9.1652284953979; __atuvc=30%7C19; __atuvs=627bdb98682bc242006' - } - }) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(html) - const rows = $('table.gr_row').toArray() - items = items.concat(rows) - } - - console.log(`Found ${items.length} channels`) - - return items.map(item => { - const $item = cheerio.load(item) - const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr( - 'alt' - ) - const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href') - const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null] - return { - lang: 'fr', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('.titre').first().text().trim() -} - -function parseStart($item, date) { - const time = $item('.heure').text() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'America/Toronto' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - '#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]' - ).toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tvhebdo.com', + days: 2, + url: function ({ channel, date }) { + return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + } + prev.stop = start + } + let stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let items = [] + const offsets = [ + 0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360 + ] + for (let offset of offsets) { + const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00` + console.log(url) + const html = await axios + .get(url, { + headers: { + Cookie: + 'distributeur=8004264; __utmz=222163677.1652094266.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _gcl_au=1.1.656635701.1652094273; tvh=3c2kaml9u14m83v91bg4dqgaf3; __utmc=222163677; IR_gbd=tvhebdo.com; IR_MPI=cf76b363-cf87-11ec-93f5-13daf79f8f76%7C1652367602625; __utma=222163677.2064368965.1652094266.1652281202.1652281479.3; __utmt=1; IR_MPS=1652284935955%7C1652284314367; _uetsid=0d8e2e60d13b11ec850db551304ae9e7; _uetvid=80456fa0b26e11ec9bf94951ce79b5f8; __utmb=222163677.19.9.1652284953979; __atuvc=30%7C19; __atuvs=627bdb98682bc242006' + } + }) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(html) + const rows = $('table.gr_row').toArray() + items = items.concat(rows) + } + + console.log(`Found ${items.length} channels`) + + return items.map(item => { + const $item = cheerio.load(item) + const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr( + 'alt' + ) + const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href') + const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null] + return { + lang: 'fr', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('.titre').first().text().trim() +} + +function parseStart($item, date) { + const time = $item('.heure').text() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'America/Toronto' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + '#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]' + ).toArray() +} diff --git a/sites/tvhebdo.com/tvhebdo.com.test.js b/sites/tvhebdo.com/tvhebdo.com.test.js index 960112b1c..4bf559a22 100644 --- a/sites/tvhebdo.com/tvhebdo.com.test.js +++ b/sites/tvhebdo.com/tvhebdo.com.test.js @@ -1,56 +1,56 @@ -// npm run channels:parse -- --config=./sites/tvhebdo.com/tvhebdo.com.config.js --output=./sites/tvhebdo.com/tvhebdo.com.channels.xml -// npm run grab -- --site=tvhebdo.com - -const { parser, url } = require('./tvhebdo.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'src/CBFT', - xmltv_id: 'CBFT.ca' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvhebdo.com/horaire-tele/src/CBFT/date/2022-05-11' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-11T15:30:00.000Z', - stop: '2022-05-11T16:00:00.000Z', - title: '5 chefs dans ma cuisine' - }) - - expect(results[16]).toMatchObject({ - start: '2022-05-12T04:09:00.000Z', - stop: '2022-05-12T05:19:00.000Z', - title: 'Outlander: Le chardon et le tartan' - }) - - expect(results[36]).toMatchObject({ - start: '2022-05-12T15:00:00.000Z', - stop: '2022-05-12T15:30:00.000Z', - title: 'Ricardo' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/no_content.html')) - const result = parser({ content, date }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvhebdo.com/tvhebdo.com.config.js --output=./sites/tvhebdo.com/tvhebdo.com.channels.xml +// npm run grab -- --site=tvhebdo.com + +const { parser, url } = require('./tvhebdo.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'src/CBFT', + xmltv_id: 'CBFT.ca' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvhebdo.com/horaire-tele/src/CBFT/date/2022-05-11' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-11T15:30:00.000Z', + stop: '2022-05-11T16:00:00.000Z', + title: '5 chefs dans ma cuisine' + }) + + expect(results[16]).toMatchObject({ + start: '2022-05-12T04:09:00.000Z', + stop: '2022-05-12T05:19:00.000Z', + title: 'Outlander: Le chardon et le tartan' + }) + + expect(results[36]).toMatchObject({ + start: '2022-05-12T15:00:00.000Z', + stop: '2022-05-12T15:30:00.000Z', + title: 'Ricardo' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/no_content.html')) + const result = parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvheute.at/tvheute.at.config.js b/sites/tvheute.at/tvheute.at.config.js index f1796a36f..500c91ba2 100644 --- a/sites/tvheute.at/tvheute.at.config.js +++ b/sites/tvheute.at/tvheute.at.config.js @@ -1,73 +1,73 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvheute.at', - days: 2, - url({ channel, date }) { - return `https://tvheute.at/part/channel-shows/partial/${channel.site_id}/${date.format( - 'DD-MM-YYYY' - )}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - icon: parseIcon(item), - category: parseCategory(item), - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - }) - - return programs - } -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('.title-col strong').text() -} - -function parseDescription(item) { - const $ = cheerio.load(item) - - return $('.title-col .description').text() -} - -function parseCategory(item) { - const $ = cheerio.load(item) - - return $('.station-col > .type').text() -} - -function parseIcon(item) { - const $ = cheerio.load(item) - const imgSrc = $('.title-col .image img').data('src-desktop') - - return imgSrc ? `https://tvheute.at${imgSrc}` : null -} - -function parseStart(item) { - const $ = cheerio.load(item) - const time = $('.end-col > .duration-wrapper').data('start') - - return dayjs(time) -} - -function parseStop(item) { - const $ = cheerio.load(item) - const time = $('.end-col > .duration-wrapper').data('stop') - - return dayjs(time) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#showListContainer > table > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvheute.at', + days: 2, + url({ channel, date }) { + return `https://tvheute.at/part/channel-shows/partial/${channel.site_id}/${date.format( + 'DD-MM-YYYY' + )}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + icon: parseIcon(item), + category: parseCategory(item), + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + }) + + return programs + } +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('.title-col strong').text() +} + +function parseDescription(item) { + const $ = cheerio.load(item) + + return $('.title-col .description').text() +} + +function parseCategory(item) { + const $ = cheerio.load(item) + + return $('.station-col > .type').text() +} + +function parseIcon(item) { + const $ = cheerio.load(item) + const imgSrc = $('.title-col .image img').data('src-desktop') + + return imgSrc ? `https://tvheute.at${imgSrc}` : null +} + +function parseStart(item) { + const $ = cheerio.load(item) + const time = $('.end-col > .duration-wrapper').data('start') + + return dayjs(time) +} + +function parseStop(item) { + const $ = cheerio.load(item) + const time = $('.end-col > .duration-wrapper').data('stop') + + return dayjs(time) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#showListContainer > table > tbody > tr').toArray() +} diff --git a/sites/tvheute.at/tvheute.at.test.js b/sites/tvheute.at/tvheute.at.test.js index c513936f0..fb5905b1c 100644 --- a/sites/tvheute.at/tvheute.at.test.js +++ b/sites/tvheute.at/tvheute.at.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=tvheute.at - -const { parser, url } = require('./tvheute.at.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' } -const content = ` -

        Das ORF1 Programm mit allen Sendungen live im TV von tv.orf.at. Sie haben eine Sendung verpasst. In der ORF TVthek finden Sie viele Sendungen on demand zum Abruf als online Video und live stream.

        ORF1 heute

        SKYsp2 ORF2
        Sender Zeit Zeit Titel Start Titel
        ORF1 Kids
        Monchhichi (Wh.) ANIMATIONSSERIE Der Streiche-Wettbewerb
        Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.
        ORF1
        ZIB 18 NACHRICHTEN
        -` - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021' - ) -}) - -it('can parse response', () => { - expect(parser({ date, channel, content })).toMatchObject([ - { - start: '2021-11-08T05:00:00.000Z', - stop: '2021-11-08T05:10:00.000Z', - title: 'Monchhichi (Wh.)', - category: 'Kids', - description: - 'Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.', - icon: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg' - }, - { - start: '2021-11-08T17:00:00.000Z', - stop: '2021-11-08T17:10:00.000Z', - title: 'ZIB 18' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: `Object moved -

        Object moved to here.

        -` - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvheute.at + +const { parser, url } = require('./tvheute.at.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' } +const content = ` +

        Das ORF1 Programm mit allen Sendungen live im TV von tv.orf.at. Sie haben eine Sendung verpasst. In der ORF TVthek finden Sie viele Sendungen on demand zum Abruf als online Video und live stream.

        ORF1 heute

        SKYsp2 ORF2
        Sender Zeit Zeit Titel Start Titel
        ORF1 Kids
        Monchhichi (Wh.) ANIMATIONSSERIE Der Streiche-Wettbewerb
        Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.
        ORF1
        ZIB 18 NACHRICHTEN
        +` + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021' + ) +}) + +it('can parse response', () => { + expect(parser({ date, channel, content })).toMatchObject([ + { + start: '2021-11-08T05:00:00.000Z', + stop: '2021-11-08T05:10:00.000Z', + title: 'Monchhichi (Wh.)', + category: 'Kids', + description: + 'Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.', + icon: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg' + }, + { + start: '2021-11-08T17:00:00.000Z', + stop: '2021-11-08T17:10:00.000Z', + title: 'ZIB 18' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: `Object moved +

        Object moved to here.

        +` + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvim.tv/tvim.tv.config.js b/sites/tvim.tv/tvim.tv.config.js index 9d0b40953..37fd3b044 100644 --- a/sites/tvim.tv/tvim.tv.config.js +++ b/sites/tvim.tv/tvim.tv.config.js @@ -1,43 +1,43 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tvim.tv', - days: 2, - url: function ({ date, channel }) { - return `https://www.tvim.tv/script/program_epg?date=${date.format('DD.MM.YYYY')}&prog=${ - channel.site_id - }&server_time=true` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - - programs.push({ - title: item.title, - description: item.desc, - category: item.genre, - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.unix(item.from_utc) -} - -function parseStop(item) { - return dayjs.unix(item.end_utc) -} - -function parseItems(content) { - const parsed = JSON.parse(content) - - return parsed.data.prog || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tvim.tv', + days: 2, + url: function ({ date, channel }) { + return `https://www.tvim.tv/script/program_epg?date=${date.format('DD.MM.YYYY')}&prog=${ + channel.site_id + }&server_time=true` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + + programs.push({ + title: item.title, + description: item.desc, + category: item.genre, + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.unix(item.from_utc) +} + +function parseStop(item) { + return dayjs.unix(item.end_utc) +} + +function parseItems(content) { + const parsed = JSON.parse(content) + + return parsed.data.prog || [] +} diff --git a/sites/tvim.tv/tvim.tv.test.js b/sites/tvim.tv/tvim.tv.test.js index 5d639978c..6d98e9d9b 100644 --- a/sites/tvim.tv/tvim.tv.test.js +++ b/sites/tvim.tv/tvim.tv.test.js @@ -1,42 +1,42 @@ -// npm run grab -- --site=tvim.tv - -const { parser, url } = require('./tvim.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'T7', xmltv_id: 'T7.rs' } -const content = - '{"response":"ok","data":{"thumb":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_rel":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large_rel":"https://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","thumb_http":"http://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large":"http://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","server_time":1635100951,"catchup_length":2,"_id":"T73","ind":2,"genre":"national","name":"T7","epg_id":"T7","chan":"T7","prog":[{"id":"T7-1635026400","title":"Programi i T7","from":1635026400,"end":1635040800,"starting":"00:00","from_utc":1635026400,"end_utc":1635040800,"desc":"Programi i T7","genre":"test","chan":"T7","epg_id":"T7","eng":""}]}}' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.tvim.tv/script/program_epg?date=24.10.2021&prog=T7&server_time=true' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: 'Sat, 23 Oct 2021 22:00:00 GMT', - stop: 'Sun, 24 Oct 2021 02:00:00 GMT', - title: 'Programi i T7', - description: 'Programi i T7', - category: 'test' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"response":"ok","data":{"server_time":1635100927}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvim.tv + +const { parser, url } = require('./tvim.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'T7', xmltv_id: 'T7.rs' } +const content = + '{"response":"ok","data":{"thumb":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_rel":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large_rel":"https://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","thumb_http":"http://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large":"http://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","server_time":1635100951,"catchup_length":2,"_id":"T73","ind":2,"genre":"national","name":"T7","epg_id":"T7","chan":"T7","prog":[{"id":"T7-1635026400","title":"Programi i T7","from":1635026400,"end":1635040800,"starting":"00:00","from_utc":1635026400,"end_utc":1635040800,"desc":"Programi i T7","genre":"test","chan":"T7","epg_id":"T7","eng":""}]}}' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.tvim.tv/script/program_epg?date=24.10.2021&prog=T7&server_time=true' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: 'Sat, 23 Oct 2021 22:00:00 GMT', + stop: 'Sun, 24 Oct 2021 02:00:00 GMT', + title: 'Programi i T7', + description: 'Programi i T7', + category: 'test' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"response":"ok","data":{"server_time":1635100927}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tving.com/tving.com.config.js b/sites/tving.com/tving.com.config.js index 57cb04b4e..7e9ce9dc0 100644 --- a/sites/tving.com/tving.com.config.js +++ b/sites/tving.com/tving.com.config.js @@ -1,94 +1,94 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tving.com', - days: 2, - url: function ({ channel, date }) { - return `https://api.tving.com/v2/media/schedules/${channel.site_id}/${date.format( - 'YYYYMMDD' - )}?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.program.name.ko, - description: item.program.synopsis.ko, - categories: parseCategories(item), - date: item.program.product_year, - directors: item.program.director, - actors: item.program.actor, - start: parseStart(item), - stop: parseStop(item), - icon: parseIcon(item) - }) - }) - - return programs - }, - async channels() { - let items = await axios - .get('https://m.tving.com/guide/schedule.tving') - .then(r => r.data) - .then(html => { - let $ = cheerio.load(html) - - return $('ul.cb > li').toArray() - }) - .catch(console.log) - - return items.map(item => { - let $item = cheerio.load(item) - let [, site_id] = $item('a') - .attr('href') - .match(/\?id=(.*)/) || [null, null] - let name = $item('img').attr('alt') - - return { - lang: 'ko', - site_id, - name - } - }) - } -} - -function parseIcon(item) { - return item.program.image.length ? `https://image.tving.com${item.program.image[0].url}` : null -} - -function parseStart(item) { - return dayjs.tz(item.broadcast_start_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') -} - -function parseStop(item) { - return dayjs.tz(item.broadcast_end_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') -} - -function parseCategories(item) { - const categories = [] - - if (item.category1_name) categories.push(item.category1_name.ko) - if (item.category2_name) categories.push(item.category2_name.ko) - - return categories.filter(Boolean) -} - -function parseItems(content) { - let data = (content.match(/cb\((.*)\)/) || [null, null])[1] - if (!data) return [] - let json = JSON.parse(data) - if (!json || !json.body || !Array.isArray(json.body.result)) return [] - - return json.body.result -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tving.com', + days: 2, + url: function ({ channel, date }) { + return `https://api.tving.com/v2/media/schedules/${channel.site_id}/${date.format( + 'YYYYMMDD' + )}?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.program.name.ko, + description: item.program.synopsis.ko, + categories: parseCategories(item), + date: item.program.product_year, + directors: item.program.director, + actors: item.program.actor, + start: parseStart(item), + stop: parseStop(item), + icon: parseIcon(item) + }) + }) + + return programs + }, + async channels() { + let items = await axios + .get('https://m.tving.com/guide/schedule.tving') + .then(r => r.data) + .then(html => { + let $ = cheerio.load(html) + + return $('ul.cb > li').toArray() + }) + .catch(console.log) + + return items.map(item => { + let $item = cheerio.load(item) + let [, site_id] = $item('a') + .attr('href') + .match(/\?id=(.*)/) || [null, null] + let name = $item('img').attr('alt') + + return { + lang: 'ko', + site_id, + name + } + }) + } +} + +function parseIcon(item) { + return item.program.image.length ? `https://image.tving.com${item.program.image[0].url}` : null +} + +function parseStart(item) { + return dayjs.tz(item.broadcast_start_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') +} + +function parseStop(item) { + return dayjs.tz(item.broadcast_end_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') +} + +function parseCategories(item) { + const categories = [] + + if (item.category1_name) categories.push(item.category1_name.ko) + if (item.category2_name) categories.push(item.category2_name.ko) + + return categories.filter(Boolean) +} + +function parseItems(content) { + let data = (content.match(/cb\((.*)\)/) || [null, null])[1] + if (!data) return [] + let json = JSON.parse(data) + if (!json || !json.body || !Array.isArray(json.body.result)) return [] + + return json.body.result +} diff --git a/sites/tving.com/tving.com.test.js b/sites/tving.com/tving.com.test.js index 31a9dd39b..9f7cd6cea 100644 --- a/sites/tving.com/tving.com.test.js +++ b/sites/tving.com/tving.com.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/tving.com/tving.com.config.js --output=./sites/tving.com/tving.com.channels.xml -// npm run grab -- --site=tving.com - -const { parser, url } = require('./tving.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-23', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'C00551', - xmltv_id: 'tvN.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.tving.com/v2/media/schedules/C00551/20230123?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: '외계+인 1부', - description: '외계+인 1부', - icon: 'https://image.tving.com/upload/cms/caip/CAIP0200/P001661154.jpg', - date: 2022, - categories: [], - directors: ['최동훈'], - actors: ['김우빈', '류준열'], - start: '2023-01-22T13:40:00.000Z', - stop: '2023-01-22T15:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') - - expect(parser({ content })).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tving.com/tving.com.config.js --output=./sites/tving.com/tving.com.channels.xml +// npm run grab -- --site=tving.com + +const { parser, url } = require('./tving.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-23', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'C00551', + xmltv_id: 'tvN.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.tving.com/v2/media/schedules/C00551/20230123?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: '외계+인 1부', + description: '외계+인 1부', + icon: 'https://image.tving.com/upload/cms/caip/CAIP0200/P001661154.jpg', + date: 2022, + categories: [], + directors: ['최동훈'], + actors: ['김우빈', '류준열'], + start: '2023-01-22T13:40:00.000Z', + stop: '2023-01-22T15:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') + + expect(parser({ content })).toMatchObject([]) +}) diff --git a/sites/tvmi.mt/tvmi.mt.config.js b/sites/tvmi.mt/tvmi.mt.config.js index 075f23b4a..dc8bb8f60 100644 --- a/sites/tvmi.mt/tvmi.mt.config.js +++ b/sites/tvmi.mt/tvmi.mt.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'tvmi.mt', - days: 2, - url: function ({ date, channel }) { - return `https://tvmi.mt/schedule/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('div > div:nth-child(2) > div:nth-child(2),a > div:nth-child(2) > div:nth-child(2)') - .text() - .trim() -} - -function parseDescription($item) { - return $item('div > div:nth-child(2) > div:nth-child(3),a > div:nth-child(2) > div:nth-child(3)') - .text() - .trim() -} - -function parseIcon($item) { - const bg = $item('div > div:nth-child(1) > div > div,a > div:nth-child(1) > div').data('bg') - - return bg ? `https:${bg}` : null -} - -function parseStart($item, date) { - const timeString = $item( - 'div > div:nth-child(2) > div:nth-child(1),a > div:nth-child(2) > div:nth-child(1)' - ) - .text() - .trim() - const [, HH, mm] = timeString.match(/^(\d{2}):(\d{2})/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Malta') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('body > main > div.mt-8 > div').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'tvmi.mt', + days: 2, + url: function ({ date, channel }) { + return `https://tvmi.mt/schedule/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('div > div:nth-child(2) > div:nth-child(2),a > div:nth-child(2) > div:nth-child(2)') + .text() + .trim() +} + +function parseDescription($item) { + return $item('div > div:nth-child(2) > div:nth-child(3),a > div:nth-child(2) > div:nth-child(3)') + .text() + .trim() +} + +function parseIcon($item) { + const bg = $item('div > div:nth-child(1) > div > div,a > div:nth-child(1) > div').data('bg') + + return bg ? `https:${bg}` : null +} + +function parseStart($item, date) { + const timeString = $item( + 'div > div:nth-child(2) > div:nth-child(1),a > div:nth-child(2) > div:nth-child(1)' + ) + .text() + .trim() + const [, HH, mm] = timeString.match(/^(\d{2}):(\d{2})/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Malta') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('body > main > div.mt-8 > div').toArray() +} diff --git a/sites/tvmi.mt/tvmi.mt.test.js b/sites/tvmi.mt/tvmi.mt.test.js index 675ff2c66..276ea8960 100644 --- a/sites/tvmi.mt/tvmi.mt.test.js +++ b/sites/tvmi.mt/tvmi.mt.test.js @@ -1,53 +1,53 @@ -// npm run grab -- --site=tvmi.mt - -const { parser, url } = require('./tvmi.mt.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'TVM.mt' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://tvmi.mt/schedule/2/2022-10-29') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T03:30:00.000Z', - stop: '2022-10-29T04:00:00.000Z', - title: 'Bizzilla', - description: - 'Storja ta’ tliet familji, tnejn minnhom miżżewġin bejniethom, u familja oħra li għalkemm mhijiex, b’daqshekk ma jfissirx li mhijiex parti ntegrali fil-kompliċitá li ilha għaddejja bejniethom għal dawn l-aħħar tletin sena.', - icon: 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/663927/image.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2022-10-29T04:00:00.000Z', - stop: '2022-10-29T04:30:00.000Z', - title: 'The Adventures of Puss in Boots', - icon: 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/747336/image.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvmi.mt + +const { parser, url } = require('./tvmi.mt.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'TVM.mt' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://tvmi.mt/schedule/2/2022-10-29') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T03:30:00.000Z', + stop: '2022-10-29T04:00:00.000Z', + title: 'Bizzilla', + description: + 'Storja ta’ tliet familji, tnejn minnhom miżżewġin bejniethom, u familja oħra li għalkemm mhijiex, b’daqshekk ma jfissirx li mhijiex parti ntegrali fil-kompliċitá li ilha għaddejja bejniethom għal dawn l-aħħar tletin sena.', + icon: 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/663927/image.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2022-10-29T04:00:00.000Z', + stop: '2022-10-29T04:30:00.000Z', + title: 'The Adventures of Puss in Boots', + icon: 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/747336/image.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvmusor.hu/tvmusor.hu.config.js b/sites/tvmusor.hu/tvmusor.hu.config.js index 9672ab61a..373e47741 100644 --- a/sites/tvmusor.hu/tvmusor.hu.config.js +++ b/sites/tvmusor.hu/tvmusor.hu.config.js @@ -1,81 +1,81 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const _ = require('lodash') - -module.exports = { - site: 'tvmusor.hu', - days: 2, - url: 'http://www.tvmusor.hu/a/get-events/', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append( - 'data', - JSON.stringify({ - blocks: [`${channel.site_id}|${date.format('YYYY-MM-DD')}`] - }) - ) - - return params - } - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = dayjs(item.e) - let stop = dayjs(item.f) - if (prev) { - start = prev.stop - } - - programs.push({ - title: item.j, - category: item.h, - description: item.c, - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('http://www.tvmusor.hu/most/') - .then(r => r.data) - .catch(console.log) - - const [, channelData] = data.match(/const CHANNEL_DATA = (.*);/) - const json = channelData.replace('},}', '}}').replace(/(\d+):/g, '"$1":') - const channels = JSON.parse(json) - - return Object.values(channels).map(item => { - return { - lang: 'hu', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseIcon(item) { - return item.z ? `http://www.tvmusor.hu/images/events/408/${item.z}` : null -} - -function parseItems(content, channel, date) { - const data = JSON.parse(content) - if (!data || !data.data || !data.data.loadedBlocks) return [] - const blocks = data.data.loadedBlocks - const blockId = `${channel.site_id}_${date.format('YYYY-MM-DD')}` - if (!Array.isArray(blocks[blockId])) return [] - - return _.uniqBy(_.uniqBy(blocks[blockId], 'e'), 'b') -} +const axios = require('axios') +const dayjs = require('dayjs') +const _ = require('lodash') + +module.exports = { + site: 'tvmusor.hu', + days: 2, + url: 'http://www.tvmusor.hu/a/get-events/', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append( + 'data', + JSON.stringify({ + blocks: [`${channel.site_id}|${date.format('YYYY-MM-DD')}`] + }) + ) + + return params + } + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = dayjs(item.e) + let stop = dayjs(item.f) + if (prev) { + start = prev.stop + } + + programs.push({ + title: item.j, + category: item.h, + description: item.c, + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('http://www.tvmusor.hu/most/') + .then(r => r.data) + .catch(console.log) + + const [, channelData] = data.match(/const CHANNEL_DATA = (.*);/) + const json = channelData.replace('},}', '}}').replace(/(\d+):/g, '"$1":') + const channels = JSON.parse(json) + + return Object.values(channels).map(item => { + return { + lang: 'hu', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseIcon(item) { + return item.z ? `http://www.tvmusor.hu/images/events/408/${item.z}` : null +} + +function parseItems(content, channel, date) { + const data = JSON.parse(content) + if (!data || !data.data || !data.data.loadedBlocks) return [] + const blocks = data.data.loadedBlocks + const blockId = `${channel.site_id}_${date.format('YYYY-MM-DD')}` + if (!Array.isArray(blocks[blockId])) return [] + + return _.uniqBy(_.uniqBy(blocks[blockId], 'e'), 'b') +} diff --git a/sites/tvmusor.hu/tvmusor.hu.test.js b/sites/tvmusor.hu/tvmusor.hu.test.js index d04630d28..7aec3b648 100644 --- a/sites/tvmusor.hu/tvmusor.hu.test.js +++ b/sites/tvmusor.hu/tvmusor.hu.test.js @@ -1,71 +1,71 @@ -// npm run channels:parse -- --config=./sites/tvmusor.hu/tvmusor.hu.config.js --output=./sites/tvmusor.hu/tvmusor.hu.channels.xml -// npm run grab -- --site=tvmusor.hu - -const { parser, url, request } = require('./tvmusor.hu.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '290', - xmltv_id: 'M4Sport.hu' -} - -it('can generate valid url', () => { - expect(url).toBe('http://www.tvmusor.hu/a/get-events/') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.get('data')).toBe('{"blocks":["290|2022-11-19"]}') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-18T23:30:00.000Z', - stop: '2022-11-19T00:55:00.000Z', - title: 'Rövidpályás Úszó Országos Bajnokság', - category: 'sportműsor', - description: 'Forma-1 magazin. Hírek, információk, érdekességek a Forma-1 világából.', - icon: 'http://www.tvmusor.hu/images/events/408/f1e45193930943d9ee29769e0afa902aff0e4a90-better-call-saul.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-19T00:55:00.000Z', - stop: '2022-11-19T01:10:00.000Z', - title: 'Sportlövészet', - category: 'sportműsor' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"status":"error","reason":"invalid blocks"}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvmusor.hu/tvmusor.hu.config.js --output=./sites/tvmusor.hu/tvmusor.hu.channels.xml +// npm run grab -- --site=tvmusor.hu + +const { parser, url, request } = require('./tvmusor.hu.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '290', + xmltv_id: 'M4Sport.hu' +} + +it('can generate valid url', () => { + expect(url).toBe('http://www.tvmusor.hu/a/get-events/') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.get('data')).toBe('{"blocks":["290|2022-11-19"]}') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-18T23:30:00.000Z', + stop: '2022-11-19T00:55:00.000Z', + title: 'Rövidpályás Úszó Országos Bajnokság', + category: 'sportműsor', + description: 'Forma-1 magazin. Hírek, információk, érdekességek a Forma-1 világából.', + icon: 'http://www.tvmusor.hu/images/events/408/f1e45193930943d9ee29769e0afa902aff0e4a90-better-call-saul.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-19T00:55:00.000Z', + stop: '2022-11-19T01:10:00.000Z', + title: 'Sportlövészet', + category: 'sportműsor' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"status":"error","reason":"invalid blocks"}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvpassport.com/tvpassport.com.config.js b/sites/tvpassport.com/tvpassport.com.config.js index 2478da47d..95b4ac227 100644 --- a/sites/tvpassport.com/tvpassport.com.config.js +++ b/sites/tvpassport.com/tvpassport.com.config.js @@ -1,156 +1,156 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvpassport.com', - days: 3, - url({ channel, date }) { - return `https://www.tvpassport.com/tv-listings/stations/${channel.site_id}/${date.format( - 'YYYY-MM-DD' - )}` - }, - request: { - headers: { - Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const start = parseStart($item) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - let title = parseTitle($item) - let sub_title = parseSubTitle($item) - if (title === 'Movie') { - title = sub_title - sub_title = null - } - - programs.push({ - title, - sub_title, - description: parseDescription($item), - icon: parseIcon($item), - category: parseCategory($item), - rating: parseRating($item), - actors: parseActors($item), - guest: parseGuest($item), - director: parseDirector($item), - start, - stop - }) - } - - return programs - }, - async channels() { - const content = await axios - .get('https://www.tvpassport.com/tv-listings', { - headers: { - Cookie: 'cisession=317b3a464bfe449650b7cc4b16ccf900a6646d88;' - } - }) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(content) - - return $('.channel_cell') - .map((i, el) => { - const site_id = $(el) - .find('a') - .attr('href') - .replace('https://www.tvpassport.com/tv-listings/stations/', '') - const name = $(el).find('.sr-only').text().trim() - - return { - site_id, - name - } - }) - .get() - } -} - -function parseDescription($item) { - return $item('*').data('description') -} - -function parseIcon($item) { - const showpicture = $item('*').data('showpicture') - const url = new URL(showpicture, 'https://cdn.tvpassport.com/image/show/960x540/') - - return url.href -} - -function parseTitle($item) { - return $item('*').data('showname') -} - -function parseSubTitle($item) { - return $item('*').data('episodetitle') -} - -function parseCategory($item) { - const showtype = $item('*').data('showtype') - - return showtype ? showtype.split(', ') : [] -} - -function parseActors($item) { - const cast = $item('*').data('cast') - - return cast ? cast.split(', ') : [] -} - -function parseDirector($item) { - const director = $item('*').data('director') - - return director ? director.split(', ') : [] -} - -function parseGuest($item) { - const guest = $item('*').data('guest') - - return guest ? guest.split(', ') : [] -} - -function parseRating($item) { - const rating = $item('*').data('rating') - - return rating - ? { - system: 'MPA', - value: rating.replace(/^TV/, 'TV-') - } - : null -} - -function parseStart($item) { - const time = $item('*').data('st') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'America/New_York') -} - -function parseDuration($item) { - const duration = $item('*').data('duration') - - return parseInt(duration) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('.station-listings .list-group-item').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvpassport.com', + days: 3, + url({ channel, date }) { + return `https://www.tvpassport.com/tv-listings/stations/${channel.site_id}/${date.format( + 'YYYY-MM-DD' + )}` + }, + request: { + headers: { + Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const start = parseStart($item) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + let title = parseTitle($item) + let sub_title = parseSubTitle($item) + if (title === 'Movie') { + title = sub_title + sub_title = null + } + + programs.push({ + title, + sub_title, + description: parseDescription($item), + icon: parseIcon($item), + category: parseCategory($item), + rating: parseRating($item), + actors: parseActors($item), + guest: parseGuest($item), + director: parseDirector($item), + start, + stop + }) + } + + return programs + }, + async channels() { + const content = await axios + .get('https://www.tvpassport.com/tv-listings', { + headers: { + Cookie: 'cisession=317b3a464bfe449650b7cc4b16ccf900a6646d88;' + } + }) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(content) + + return $('.channel_cell') + .map((i, el) => { + const site_id = $(el) + .find('a') + .attr('href') + .replace('https://www.tvpassport.com/tv-listings/stations/', '') + const name = $(el).find('.sr-only').text().trim() + + return { + site_id, + name + } + }) + .get() + } +} + +function parseDescription($item) { + return $item('*').data('description') +} + +function parseIcon($item) { + const showpicture = $item('*').data('showpicture') + const url = new URL(showpicture, 'https://cdn.tvpassport.com/image/show/960x540/') + + return url.href +} + +function parseTitle($item) { + return $item('*').data('showname') +} + +function parseSubTitle($item) { + return $item('*').data('episodetitle') +} + +function parseCategory($item) { + const showtype = $item('*').data('showtype') + + return showtype ? showtype.split(', ') : [] +} + +function parseActors($item) { + const cast = $item('*').data('cast') + + return cast ? cast.split(', ') : [] +} + +function parseDirector($item) { + const director = $item('*').data('director') + + return director ? director.split(', ') : [] +} + +function parseGuest($item) { + const guest = $item('*').data('guest') + + return guest ? guest.split(', ') : [] +} + +function parseRating($item) { + const rating = $item('*').data('rating') + + return rating + ? { + system: 'MPA', + value: rating.replace(/^TV/, 'TV-') + } + : null +} + +function parseStart($item) { + const time = $item('*').data('st') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'America/New_York') +} + +function parseDuration($item) { + const duration = $item('*').data('duration') + + return parseInt(duration) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('.station-listings .list-group-item').toArray() +} diff --git a/sites/tvpassport.com/tvpassport.com.test.js b/sites/tvpassport.com/tvpassport.com.test.js index 9e33be7d6..6469f8ffc 100644 --- a/sites/tvpassport.com/tvpassport.com.test.js +++ b/sites/tvpassport.com/tvpassport.com.test.js @@ -1,63 +1,63 @@ -// npm run grab -- --site=tvpassport.com -// npm run channels:parse -- --config=./sites/tvpassport.com/tvpassport.com.config.js --output=./sites/tvpassport.com/tvpassport.com.channels.xml - -const { parser, url, request } = require('./tvpassport.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'youtoo-america-network/5463', - xmltv_id: 'YTATV.us' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvpassport.com/tv-listings/stations/youtoo-america-network/5463/2022-10-04' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-04T10:00:00.000Z', - stop: '2022-10-04T10:30:00.000Z', - title: 'Charlie Moore: No Offense', - sub_title: 'Under the Influencer', - category: ['Sports', 'Outdoors'], - icon: 'https://cdn.tvpassport.com/image/show/960x540/69103.jpg', - rating: { - system: 'MPA', - value: 'TV-G' - }, - actors: ['John Reardon', 'Mayko Nguyen', 'Justin Kelly'], - director: ['Rob McElhenney'], - guest: ['Sean Penn'], - description: - 'Celebrity interviews while fishing in various locations throughout the United States.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '' }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvpassport.com +// npm run channels:parse -- --config=./sites/tvpassport.com/tvpassport.com.config.js --output=./sites/tvpassport.com/tvpassport.com.channels.xml + +const { parser, url, request } = require('./tvpassport.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'youtoo-america-network/5463', + xmltv_id: 'YTATV.us' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvpassport.com/tv-listings/stations/youtoo-america-network/5463/2022-10-04' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-04T10:00:00.000Z', + stop: '2022-10-04T10:30:00.000Z', + title: 'Charlie Moore: No Offense', + sub_title: 'Under the Influencer', + category: ['Sports', 'Outdoors'], + icon: 'https://cdn.tvpassport.com/image/show/960x540/69103.jpg', + rating: { + system: 'MPA', + value: 'TV-G' + }, + actors: ['John Reardon', 'Mayko Nguyen', 'Justin Kelly'], + director: ['Rob McElhenney'], + guest: ['Sean Penn'], + description: + 'Celebrity interviews while fishing in various locations throughout the United States.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvplus.com.tr/tvplus.com.tr.config.js b/sites/tvplus.com.tr/tvplus.com.tr.config.js index 39e78a75b..c460fd5de 100644 --- a/sites/tvplus.com.tr/tvplus.com.tr.config.js +++ b/sites/tvplus.com.tr/tvplus.com.tr.config.js @@ -1,74 +1,74 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvplus.com.tr', - days: 2, - url: 'https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/PlayBillList', - request: { - method: 'POST', - async headers() { - const response = await axios - .post('https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/Authenticate', { - terminaltype: 'WEBTV_WIDEVINE', - userType: '3', - timezone: 'UTC' - }) - .catch(console.log) - const cookie = Array.isArray(response.headers['set-cookie']) - ? response.headers['set-cookie'].join('; ') - : '' - - return { cookie } - }, - data({ date, channel }) { - return { - type: '2', - channelid: channel.site_id, - begintime: date.format('YYYYMMDDHHmmss'), - endtime: date.add(1, 'd').format('YYYYMMDDHHmmss') - } - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title: item.name, - category: item.genres, - description: item.introduce, - icon: parseIcon(item), - start: start.toJSON(), - stop: stop.toJSON() - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.pictures && item.pictures.length ? item.pictures[0].href : null -} - -function parseStart(item) { - return dayjs.utc(item.starttime, 'YYYYMMDDHHmmss') -} - -function parseStop(item) { - return dayjs.utc(item.endtime, 'YYYYMMDDHHmmss') -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.playbilllist || [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvplus.com.tr', + days: 2, + url: 'https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/PlayBillList', + request: { + method: 'POST', + async headers() { + const response = await axios + .post('https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/Authenticate', { + terminaltype: 'WEBTV_WIDEVINE', + userType: '3', + timezone: 'UTC' + }) + .catch(console.log) + const cookie = Array.isArray(response.headers['set-cookie']) + ? response.headers['set-cookie'].join('; ') + : '' + + return { cookie } + }, + data({ date, channel }) { + return { + type: '2', + channelid: channel.site_id, + begintime: date.format('YYYYMMDDHHmmss'), + endtime: date.add(1, 'd').format('YYYYMMDDHHmmss') + } + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title: item.name, + category: item.genres, + description: item.introduce, + icon: parseIcon(item), + start: start.toJSON(), + stop: stop.toJSON() + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.pictures && item.pictures.length ? item.pictures[0].href : null +} + +function parseStart(item) { + return dayjs.utc(item.starttime, 'YYYYMMDDHHmmss') +} + +function parseStop(item) { + return dayjs.utc(item.endtime, 'YYYYMMDDHHmmss') +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.playbilllist || [] +} diff --git a/sites/tvplus.com.tr/tvplus.com.tr.test.js b/sites/tvplus.com.tr/tvplus.com.tr.test.js index 820762c4b..49019d139 100644 --- a/sites/tvplus.com.tr/tvplus.com.tr.test.js +++ b/sites/tvplus.com.tr/tvplus.com.tr.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=tvplus.com.tr - -const { parser, url, request } = require('./tvplus.com.tr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '32', - xmltv_id: '24TV.tr' -} -const content = - '{"counttotal":"23","playbilllist":[{"country":"","starttime":"20211107000000","type":"PROGRAM","isBlackout":"0","rerun":"0","ppvsubscribed":"0","foreignsn":"134383557","isLive":"0","ratingid":"0","episodeTotalCount":"0","id":"134383557","keyword":"24 Portre","contentType":"0","isnpvr":"1","slsType":"0","iscpvr":"0","advisory":[],"genreIds":["1179"],"istvod":"0","name":"24 Portre","tvodStatus":"0","pictures":[{"href":"https://izmottvsc23.tvplus.com.tr:33207/CPS/images/universal/film/program/202111/20211104/35/20211104000026695lh5.jpg","resolution":["null","null"],"imageType":"0"}],"externalContentCode":"105445035962202111070300","genres":"Yaşam","visittimes":"0","issubscribed":"0","programType":"program","gapFiller":"0","introduce":"Kendi alanında büyük başarılar elde etmiş insanların kendi ağzından hayat hikayeleri ekrana geliyor.","priceType":[{"value":"0","key":"BTV"},{"value":"0","key":"TVOD"}],"endtime":"20211107010000","seasonTotalCount":"0","recordedMediaIds":[],"picture":{},"isLoyalty":"0","isppv":"0","mainGenre":"0","contentRight":"[{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"13\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"14\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"15\\",\\"enable\\":\\"1\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"13\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"14\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"15\\",\\"enable\\":\\"1\\"}]","channelid":"32"}],"playbillVersion":[{"channelId":"32","date":"20211108","version":"20211106000043"},{"channelId":"32","date":"20211107","version":"20211105000027"}]}' - -it('can generate valid url', () => { - expect(url).toBe('https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/PlayBillList') -}) - -it('can generate valid request data', () => { - const result = request.data({ date, channel }) - expect(result).toMatchObject({ - type: '2', - channelid: '32', - begintime: '20211107000000', - endtime: '20211108000000' - }) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-07T00:00:00.000Z', - stop: '2021-11-07T01:00:00.000Z', - title: '24 Portre', - category: 'Yaşam', - icon: 'https://izmottvsc23.tvplus.com.tr:33207/CPS/images/universal/film/program/202111/20211104/35/20211104000026695lh5.jpg', - description: - 'Kendi alanında büyük başarılar elde etmiş insanların kendi ağzından hayat hikayeleri ekrana geliyor.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"counttotal":"0","playbilllist":[],"playbillVersion":[{"channelId":"10000","date":"20211108","version":"20211107163253"},{"channelId":"10000","date":"20211107","version":"20211107163253"}]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvplus.com.tr + +const { parser, url, request } = require('./tvplus.com.tr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '32', + xmltv_id: '24TV.tr' +} +const content = + '{"counttotal":"23","playbilllist":[{"country":"","starttime":"20211107000000","type":"PROGRAM","isBlackout":"0","rerun":"0","ppvsubscribed":"0","foreignsn":"134383557","isLive":"0","ratingid":"0","episodeTotalCount":"0","id":"134383557","keyword":"24 Portre","contentType":"0","isnpvr":"1","slsType":"0","iscpvr":"0","advisory":[],"genreIds":["1179"],"istvod":"0","name":"24 Portre","tvodStatus":"0","pictures":[{"href":"https://izmottvsc23.tvplus.com.tr:33207/CPS/images/universal/film/program/202111/20211104/35/20211104000026695lh5.jpg","resolution":["null","null"],"imageType":"0"}],"externalContentCode":"105445035962202111070300","genres":"Yaşam","visittimes":"0","issubscribed":"0","programType":"program","gapFiller":"0","introduce":"Kendi alanında büyük başarılar elde etmiş insanların kendi ağzından hayat hikayeleri ekrana geliyor.","priceType":[{"value":"0","key":"BTV"},{"value":"0","key":"TVOD"}],"endtime":"20211107010000","seasonTotalCount":"0","recordedMediaIds":[],"picture":{},"isLoyalty":"0","isppv":"0","mainGenre":"0","contentRight":"[{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"13\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"14\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"15\\",\\"enable\\":\\"1\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"13\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"14\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"15\\",\\"enable\\":\\"1\\"}]","channelid":"32"}],"playbillVersion":[{"channelId":"32","date":"20211108","version":"20211106000043"},{"channelId":"32","date":"20211107","version":"20211105000027"}]}' + +it('can generate valid url', () => { + expect(url).toBe('https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/PlayBillList') +}) + +it('can generate valid request data', () => { + const result = request.data({ date, channel }) + expect(result).toMatchObject({ + type: '2', + channelid: '32', + begintime: '20211107000000', + endtime: '20211108000000' + }) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-07T00:00:00.000Z', + stop: '2021-11-07T01:00:00.000Z', + title: '24 Portre', + category: 'Yaşam', + icon: 'https://izmottvsc23.tvplus.com.tr:33207/CPS/images/universal/film/program/202111/20211104/35/20211104000026695lh5.jpg', + description: + 'Kendi alanında büyük başarılar elde etmiş insanların kendi ağzından hayat hikayeleri ekrana geliyor.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"counttotal":"0","playbilllist":[],"playbillVersion":[{"channelId":"10000","date":"20211108","version":"20211107163253"},{"channelId":"10000","date":"20211107","version":"20211107163253"}]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvprofil.com/tvprofil.com.config.js b/sites/tvprofil.com/tvprofil.com.config.js index 6cf2f80de..ac568528c 100644 --- a/sites/tvprofil.com/tvprofil.com.config.js +++ b/sites/tvprofil.com/tvprofil.com.config.js @@ -1,97 +1,97 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvprofil.com', - days: 2, - url: function ({ channel, date }) { - const parts = channel.site_id.split('#') - const query = buildQuery(parts[1], date) - - return `https://tvprofil.com/${parts[0]}/program/?${query}` - }, - request: { - headers: { - 'x-requested-with': 'XMLHttpRequest' - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - const category = parseCategory($item) - const start = parseStart($item) - const duration = parseDuration($item) - const stop = start.add(duration, 's') - const icon = parseIcon($item) - - programs.push({ title, category, start, stop, icon }) - }) - - return programs - } -} - -function parseIcon($item) { - return $item(':root').data('image') -} - -function parseDuration($item) { - return $item(':root').data('len') -} - -function parseStart($item) { - const timestamp = $item(':root').data('ts') - - return dayjs.unix(timestamp) -} - -function parseCategory($item) { - return $item('.col:nth-child(2) > small').text() || null -} - -function parseTitle($item) { - let title = $item('.col:nth-child(2) > a').text() - title += $item('.col:nth-child(2)').clone().children().remove().end().text() - - return title.replace('®', '').trim().replace(/,$/, '') -} - -function parseItems(content) { - let data = (content.match(/cb\((.*)\)/) || [null, null])[1] - if (!data) return [] - let json = JSON.parse(data) - if (!json || !json.data || !json.data.program) return [] - - const $ = cheerio.load(json.data.program) - - return $('.row').toArray() -} - -function buildQuery(site_id, date) { - const query = { - datum: date.format('YYYY-MM-DD'), - kanal: site_id, - callback: 'cb' - } - - const a = query.datum + query.kanal - const ua = query.kanal + query.datum - - let i = a.length, - b = 2, - c = 2 - - for (var j = 0; j < ua.length; j++) c += ua.charCodeAt(j) - while (i--) { - b += (a.charCodeAt(i) + c * 2) * i - } - - const key = 'b' + b.toString().charCodeAt(2) - - query[key] = b - - return new URLSearchParams(query).toString() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvprofil.com', + days: 2, + url: function ({ channel, date }) { + const parts = channel.site_id.split('#') + const query = buildQuery(parts[1], date) + + return `https://tvprofil.com/${parts[0]}/program/?${query}` + }, + request: { + headers: { + 'x-requested-with': 'XMLHttpRequest' + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + const category = parseCategory($item) + const start = parseStart($item) + const duration = parseDuration($item) + const stop = start.add(duration, 's') + const icon = parseIcon($item) + + programs.push({ title, category, start, stop, icon }) + }) + + return programs + } +} + +function parseIcon($item) { + return $item(':root').data('image') +} + +function parseDuration($item) { + return $item(':root').data('len') +} + +function parseStart($item) { + const timestamp = $item(':root').data('ts') + + return dayjs.unix(timestamp) +} + +function parseCategory($item) { + return $item('.col:nth-child(2) > small').text() || null +} + +function parseTitle($item) { + let title = $item('.col:nth-child(2) > a').text() + title += $item('.col:nth-child(2)').clone().children().remove().end().text() + + return title.replace('®', '').trim().replace(/,$/, '') +} + +function parseItems(content) { + let data = (content.match(/cb\((.*)\)/) || [null, null])[1] + if (!data) return [] + let json = JSON.parse(data) + if (!json || !json.data || !json.data.program) return [] + + const $ = cheerio.load(json.data.program) + + return $('.row').toArray() +} + +function buildQuery(site_id, date) { + const query = { + datum: date.format('YYYY-MM-DD'), + kanal: site_id, + callback: 'cb' + } + + const a = query.datum + query.kanal + const ua = query.kanal + query.datum + + let i = a.length, + b = 2, + c = 2 + + for (var j = 0; j < ua.length; j++) c += ua.charCodeAt(j) + while (i--) { + b += (a.charCodeAt(i) + c * 2) * i + } + + const key = 'b' + b.toString().charCodeAt(2) + + query[key] = b + + return new URLSearchParams(query).toString() +} diff --git a/sites/tvprofil.com/tvprofil.com.test.js b/sites/tvprofil.com/tvprofil.com.test.js index 47af3e758..3019ab32a 100644 --- a/sites/tvprofil.com/tvprofil.com.test.js +++ b/sites/tvprofil.com/tvprofil.com.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=tvprofil.com - -const { parser, url, request } = require('./tvprofil.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'bg/tv-programa#24kitchen-bg', - xmltv_id: '24KitchenBulgaria.bg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tvprofil.com/bg/tv-programa/program/?datum=2023-01-12&kanal=24kitchen-bg&callback=cb&b55=747917' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-requested-with': 'XMLHttpRequest' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Мексиканска кухня с Пати 10, еп. 9', - start: '2023-01-12T04:00:00.000Z', - stop: '2023-01-12T04:30:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') - - expect(parser({ content })).toMatchObject([]) -}) +// npm run grab -- --site=tvprofil.com + +const { parser, url, request } = require('./tvprofil.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'bg/tv-programa#24kitchen-bg', + xmltv_id: '24KitchenBulgaria.bg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tvprofil.com/bg/tv-programa/program/?datum=2023-01-12&kanal=24kitchen-bg&callback=cb&b55=747917' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-requested-with': 'XMLHttpRequest' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Мексиканска кухня с Пати 10, еп. 9', + start: '2023-01-12T04:00:00.000Z', + stop: '2023-01-12T04:30:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') + + expect(parser({ content })).toMatchObject([]) +}) diff --git a/sites/tvtv.us/tvtv.us.config.js b/sites/tvtv.us/tvtv.us.config.js index 81763555d..2f615763f 100644 --- a/sites/tvtv.us/tvtv.us.config.js +++ b/sites/tvtv.us/tvtv.us.config.js @@ -1,40 +1,40 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'tvtv.us', - request: { - delay: 500 // 500 ms - }, - days: 2, - url: function ({ date, channel }) { - return `https://www.tvtv.us/api/v1/lineup/USA-NY71652-DEFAULT/grid/${date.toJSON()}/${date - .add(1, 'd') - .toJSON()}/${channel.site_id}` - }, - parser: function ({ content }) { - let programs = [] - - const items = parseItems(content) - items.forEach(item => { - const start = dayjs.utc(item.startTime) - const stop = start.add(item.runTime, 'm') - programs.push({ - title: item.title, - description: item.subtitle, - start, - stop - }) - }) - - return programs - } -} - -function parseItems(content) { - const json = JSON.parse(content) - if (!json.length) return [] - return json[0] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'tvtv.us', + request: { + delay: 500 // 500 ms + }, + days: 2, + url: function ({ date, channel }) { + return `https://www.tvtv.us/api/v1/lineup/USA-NY71652-DEFAULT/grid/${date.toJSON()}/${date + .add(1, 'd') + .toJSON()}/${channel.site_id}` + }, + parser: function ({ content }) { + let programs = [] + + const items = parseItems(content) + items.forEach(item => { + const start = dayjs.utc(item.startTime) + const stop = start.add(item.runTime, 'm') + programs.push({ + title: item.title, + description: item.subtitle, + start, + stop + }) + }) + + return programs + } +} + +function parseItems(content) { + const json = JSON.parse(content) + if (!json.length) return [] + return json[0] +} diff --git a/sites/tvtv.us/tvtv.us.test.js b/sites/tvtv.us/tvtv.us.test.js index c22d6e453..685f617cd 100644 --- a/sites/tvtv.us/tvtv.us.test.js +++ b/sites/tvtv.us/tvtv.us.test.js @@ -1,54 +1,54 @@ -// npm run grab -- --site=tvtv.us - -const { parser, url } = require('./tvtv.us.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-09-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '62670', - xmltv_id: 'AMITV.ca', - logo: 'https://tvtv.us/gn/i/assets/s62670_ll_h15_ab.png?w=360&h=270' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvtv.us/api/v1/lineup/USA-NY71652-DEFAULT/grid/2022-09-20T00:00:00.000Z/2022-09-21T00:00:00.000Z/62670' - ) -}) - -it('can parse response', () => { - const content = - '[[{"programId":"EP039131940001","title":"Beyond the Field","subtitle":"Diversity in Sport","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:00Z","start":0,"duration":30,"runTime":30},{"programId":"EP032368970002","title":"IGotThis","subtitle":"Listen to Dis","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:30Z","start":120,"duration":30,"runTime":30}]]' - - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-09-20T00:00:00.000Z', - stop: '2022-09-20T00:30:00.000Z', - title: 'Beyond the Field', - description: 'Diversity in Sport' - }, - { - start: '2022-09-20T00:30:00.000Z', - stop: '2022-09-20T01:00:00.000Z', - title: 'IGotThis', - description: 'Listen to Dis' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvtv.us + +const { parser, url } = require('./tvtv.us.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-09-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '62670', + xmltv_id: 'AMITV.ca', + logo: 'https://tvtv.us/gn/i/assets/s62670_ll_h15_ab.png?w=360&h=270' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvtv.us/api/v1/lineup/USA-NY71652-DEFAULT/grid/2022-09-20T00:00:00.000Z/2022-09-21T00:00:00.000Z/62670' + ) +}) + +it('can parse response', () => { + const content = + '[[{"programId":"EP039131940001","title":"Beyond the Field","subtitle":"Diversity in Sport","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:00Z","start":0,"duration":30,"runTime":30},{"programId":"EP032368970002","title":"IGotThis","subtitle":"Listen to Dis","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:30Z","start":120,"duration":30,"runTime":30}]]' + + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-09-20T00:00:00.000Z', + stop: '2022-09-20T00:30:00.000Z', + title: 'Beyond the Field', + description: 'Diversity in Sport' + }, + { + start: '2022-09-20T00:30:00.000Z', + stop: '2022-09-20T01:00:00.000Z', + title: 'IGotThis', + description: 'Listen to Dis' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/unifi.com.my/unifi.com.my.config.js b/sites/unifi.com.my/unifi.com.my.config.js index fb98d5eee..0996bb239 100644 --- a/sites/unifi.com.my/unifi.com.my.config.js +++ b/sites/unifi.com.my/unifi.com.my.config.js @@ -1,60 +1,60 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'unifi.com.my', - days: 2, - url: 'https://unifi.com.my/tv/api/tv', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - method: 'POST', - headers: { - 'x-requested-with': 'XMLHttpRequest' - }, - data({ date }) { - const params = new URLSearchParams() - params.append('date', date.format('YYYY-MM-DD')) - return params - } - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.minute, 'minute') - programs.push({ - title: item.name, - start, - stop - }) - }) - return programs - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!data) return [] - if (!Array.isArray(data)) return [] - - const channelData = data.find(i => i.id == channel.site_id) - return channelData.items && Array.isArray(channelData.items) ? channelData.items : [] - } catch (err) { - return [] - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.start_time}` - return dayjs.tz(time, 'YYYY-MM-DD H:mma', 'Asia/Kuala_Lumpur') -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'unifi.com.my', + days: 2, + url: 'https://unifi.com.my/tv/api/tv', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + method: 'POST', + headers: { + 'x-requested-with': 'XMLHttpRequest' + }, + data({ date }) { + const params = new URLSearchParams() + params.append('date', date.format('YYYY-MM-DD')) + return params + } + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.minute, 'minute') + programs.push({ + title: item.name, + start, + stop + }) + }) + return programs + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!data) return [] + if (!Array.isArray(data)) return [] + + const channelData = data.find(i => i.id == channel.site_id) + return channelData.items && Array.isArray(channelData.items) ? channelData.items : [] + } catch (err) { + return [] + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.start_time}` + return dayjs.tz(time, 'YYYY-MM-DD H:mma', 'Asia/Kuala_Lumpur') +} diff --git a/sites/unifi.com.my/unifi.com.my.test.js b/sites/unifi.com.my/unifi.com.my.test.js index 1ee1deb8f..276ea0f68 100644 --- a/sites/unifi.com.my/unifi.com.my.test.js +++ b/sites/unifi.com.my/unifi.com.my.test.js @@ -1,57 +1,57 @@ -// npm run grab -- --site=unifi.com.my - -const { parser, url, request } = require('./unifi.com.my.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '20000009', - xmltv_id: 'TV1.my' -} - -it('can generate valid url', () => { - expect(url).toBe('https://unifi.com.my/tv/api/tv') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-requested-with': 'XMLHttpRequest' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ date }) - - expect(data.get('date')).toBe('2023-01-13') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Berita Tengah Malam', - start: '2023-01-12T16:00:00.000Z', - stop: '2023-01-12T16:30:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '', channel }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=unifi.com.my + +const { parser, url, request } = require('./unifi.com.my.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '20000009', + xmltv_id: 'TV1.my' +} + +it('can generate valid url', () => { + expect(url).toBe('https://unifi.com.my/tv/api/tv') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-requested-with': 'XMLHttpRequest' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ date }) + + expect(data.get('date')).toBe('2023-01-13') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Berita Tengah Malam', + start: '2023-01-12T16:00:00.000Z', + stop: '2023-01-12T16:30:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '', channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/vidio.com/vidio.com.config.js b/sites/vidio.com/vidio.com.config.js index 6312ea4e4..3923d409f 100644 --- a/sites/vidio.com/vidio.com.config.js +++ b/sites/vidio.com/vidio.com.config.js @@ -1,65 +1,65 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'vidio.com', - days: 2, - url({ channel }) { - return `https://www.vidio.com/live/${channel.site_id}/schedules` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() - const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${start}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Jakarta' }).toUTC() -} - -function parseStop($item, date) { - const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() - const [, stop] = timeString.match(/- (\d{2}:\d{2}) WIB/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${stop}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Jakarta' }).toUTC() -} - -function parseTitle($item) { - return $item('div.b-livestreaming-daily-schedule__item-content-title').text() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - return $( - `#schedule-content-${date.format( - 'YYYYMMDD' - )} > .b-livestreaming-daily-schedule__scroll-container .b-livestreaming-daily-schedule__item` - ).toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'vidio.com', + days: 2, + url({ channel }) { + return `https://www.vidio.com/live/${channel.site_id}/schedules` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() + const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${start}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Jakarta' }).toUTC() +} + +function parseStop($item, date) { + const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() + const [, stop] = timeString.match(/- (\d{2}:\d{2}) WIB/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${stop}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Jakarta' }).toUTC() +} + +function parseTitle($item) { + return $item('div.b-livestreaming-daily-schedule__item-content-title').text() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + return $( + `#schedule-content-${date.format( + 'YYYYMMDD' + )} > .b-livestreaming-daily-schedule__scroll-container .b-livestreaming-daily-schedule__item` + ).toArray() +} diff --git a/sites/vidio.com/vidio.com.test.js b/sites/vidio.com/vidio.com.test.js index 2f8e7bb49..ef5bec17f 100644 --- a/sites/vidio.com/vidio.com.test.js +++ b/sites/vidio.com/vidio.com.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=vidio.com - -const { parser, url } = require('./vidio.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '7464', - xmltv_id: 'AjwaTV.id' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.vidio.com/live/7464/schedules') -}) - -it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-23T17:30:00.000Z', - stop: '2021-11-23T18:30:00.000Z', - title: '30 Hari 30 Juz' - }, - { - start: '2021-11-23T18:30:00.000Z', - stop: '2021-11-23T21:00:00.000Z', - title: 'Makkah Live' - }, - { - start: '2021-11-24T15:30:00.000Z', - stop: '2021-11-24T17:30:00.000Z', - title: 'FTV Islami' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=vidio.com + +const { parser, url } = require('./vidio.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '7464', + xmltv_id: 'AjwaTV.id' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.vidio.com/live/7464/schedules') +}) + +it('can parse response', () => { + const content = + '' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-23T17:30:00.000Z', + stop: '2021-11-23T18:30:00.000Z', + title: '30 Hari 30 Juz' + }, + { + start: '2021-11-23T18:30:00.000Z', + stop: '2021-11-23T21:00:00.000Z', + title: 'Makkah Live' + }, + { + start: '2021-11-24T15:30:00.000Z', + stop: '2021-11-24T17:30:00.000Z', + title: 'FTV Islami' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/virginmedia.com/virginmedia.com.config.js b/sites/virginmedia.com/virginmedia.com.config.js index c839f2bf6..90f1c419b 100644 --- a/sites/virginmedia.com/virginmedia.com.config.js +++ b/sites/virginmedia.com/virginmedia.com.config.js @@ -1,112 +1,112 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web' - -module.exports = { - site: 'virginmedia.com', - days: 2, - url: function ({ date }) { - return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const d = date.format('YYYYMMDD') - const promises = [ - axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) - ] - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data, channel)) - } - }) - }) - .catch(console.error) - // items.forEach(item => { - for (let item of items) { - const detail = await loadProgramDetails(item) - programs.push({ - title: item.t, - description: parseDescription(detail), - category: parseCategory(detail), - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - //) - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}/channels`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'be', - site_id: item.id.replace('lgi-gb-prodobo-master:40980-', ''), - name: item.title - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.i) return {} - const url = `${API_ENDPOINT}/listings/${item.i}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseStart(item) { - return dayjs(item.s) -} - -function parseStop(item) { - return dayjs(item.e) -} - -function parseItems(content, channel) { - const data = typeof content === 'string' ? JSON.parse(content) : content - if (!data || !Array.isArray(data.entries)) return [] - const entity = data.entries.find(e => e.o === `lgi-gb-prodobo-master:${channel.site_id}`) - - return entity ? entity.l : [] -} - -function parseDescription(detail) { - return detail.program.longDescription || null -} - -function parseCategory(detail) { - let categories = [] - detail.program.categories.forEach(category => { - categories.push(category.title) - }) - return categories -} - -function parseSeason(detail) { - if (!detail.program.seriesNumber) return null - if (String(detail.program.seriesNumber).length > 2) return null - return detail.program.seriesNumber -} - -function parseEpisode(detail) { - if (!detail.program.seriesEpisodeNumber) return null - if (String(detail.program.seriesEpisodeNumber).length > 3) return null - return detail.program.seriesEpisodeNumber -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web' + +module.exports = { + site: 'virginmedia.com', + days: 2, + url: function ({ date }) { + return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const d = date.format('YYYYMMDD') + const promises = [ + axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) + ] + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data, channel)) + } + }) + }) + .catch(console.error) + // items.forEach(item => { + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.t, + description: parseDescription(detail), + category: parseCategory(detail), + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + //) + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}/channels`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'be', + site_id: item.id.replace('lgi-gb-prodobo-master:40980-', ''), + name: item.title + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.i) return {} + const url = `${API_ENDPOINT}/listings/${item.i}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseStart(item) { + return dayjs(item.s) +} + +function parseStop(item) { + return dayjs(item.e) +} + +function parseItems(content, channel) { + const data = typeof content === 'string' ? JSON.parse(content) : content + if (!data || !Array.isArray(data.entries)) return [] + const entity = data.entries.find(e => e.o === `lgi-gb-prodobo-master:${channel.site_id}`) + + return entity ? entity.l : [] +} + +function parseDescription(detail) { + return detail.program.longDescription || null +} + +function parseCategory(detail) { + let categories = [] + detail.program.categories.forEach(category => { + categories.push(category.title) + }) + return categories +} + +function parseSeason(detail) { + if (!detail.program.seriesNumber) return null + if (String(detail.program.seriesNumber).length > 2) return null + return detail.program.seriesNumber +} + +function parseEpisode(detail) { + if (!detail.program.seriesEpisodeNumber) return null + if (String(detail.program.seriesEpisodeNumber).length > 3) return null + return detail.program.seriesEpisodeNumber +} diff --git a/sites/virginmedia.com/virginmedia.com.test.js b/sites/virginmedia.com/virginmedia.com.test.js index c8744329d..ab6728f0b 100644 --- a/sites/virginmedia.com/virginmedia.com.test.js +++ b/sites/virginmedia.com/virginmedia.com.test.js @@ -1,156 +1,156 @@ -// npm run channels:parse -- --config=./sites/virginmedia.com/virginmedia.com.config.js --output=./sites/virginmedia.com/virginmedia.com.channels.xml -// npm run grab -- --site=virginmedia.com - -const { parser, url } = require('./virginmedia.com.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-03-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1761', - xmltv_id: 'ViaplaySports1.uk' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/1' - ) -}) - -it('can parse response', done => { - const content = - '{"entryCount":410,"totalResults":410,"updated":1647459686755,"expires":1647460298218,"title":"EPG","periods":4,"periodStartTime":1647475200000,"periodEndTime":1647496800000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","t":"Live: NHL Hockey","s":1647473400000,"e":1647484200000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - - axios.get.mockImplementation(url => { - if ( - url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/2' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":410,"totalResults":410,"updated":1647460887411,"expires":1647461895572,"title":"EPG","periods":4,"periodStartTime":1647496800000,"periodEndTime":1647518400000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","t":"Challenge Cup Ice Hockey","s":1647484200000,"e":1647496800000,"c":"lgi-gb-prodobo-master:genre-123","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/3' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":410,"totalResults":410,"updated":1647460871713,"expires":1647461910282,"title":"EPG","periods":4,"periodStartTime":1647518400000,"periodEndTime":1647540000000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","t":"NHL Hockey","s":1647511200000,"e":1647518400000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/4' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":410,"totalResults":410,"updated":1647460871713,"expires":1647461920720,"title":"EPG","periods":4,"periodStartTime":1647540000000,"periodEndTime":1647561600000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","t":"Boxing World Weekly","s":1647539100000,"e":1647540900000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","startTime":1647473400000,"endTime":1647484200000,"actualStartTime":1647473400000,"actualEndTime":1647484200000,"expirationDate":1648078200000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:de610af9a9b049c8a0245173f273136d36458f6f","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005","title":"Live: NHL Hockey","description":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","longDescription":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResLandscapeProductionStill","assetTypes":["HighResLandscapeProductionStill"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p21763419_tb2_h8_aa.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p21763419_tb2_v12_aa.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH013520120000","rootId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","shortDescription":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"2022031605","seriesNumber":"20120000","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"Boston Bruins at Minnesota Wild"},"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH013520120000","rootId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647484200000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21763419|en-GB"}' - ) - }) - } else if ( - url === - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","startTime":1647484200000,"endTime":1647496800000,"actualStartTime":1647484200000,"actualEndTime":1647496800000,"expirationDate":1648089000000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005","title":"Challenge Cup Ice Hockey","description":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","longDescription":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p11743980_b_v12_aa.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH021779870000","rootId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","shortDescription":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"2022031605","seriesNumber":"79870000","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"Final: Belfast Giants v Cardiff Devils"},"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH021779870000","rootId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647928800000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21720572|en-GB"}' - ) - }) - } else if ( - url === - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","startTime":1647511200000,"endTime":1647518400000,"actualStartTime":1647511200000,"actualEndTime":1647518400000,"expirationDate":1648116000000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435","title":"NHL Hockey","description":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","longDescription":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResLandscapeProductionStill","assetTypes":["HighResLandscapeProductionStill"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p448880_b_h8_ak.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p448880_b_v12_ak.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F21275201~~2FSH012830210000","rootId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","shortDescription":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"194","seriesNumber":"102022","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"New Jersey Devils at Calgary Flames"},"parentId":"crid:~~2F~~2Fgn.tv~~2F21275201~~2FSH012830210000","rootId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647583200000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21763550|en-GB"}' - ) - }) - } else if ( - url === - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","startTime":1647539100000,"endTime":1647540900000,"actualStartTime":1647539100000,"actualEndTime":1647540900000,"expirationDate":1648143900000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:c02da14358110cec07d14dc154717ce62ba2f489","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145","title":"Boxing World Weekly","description":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","longDescription":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-83","title":"Boxing","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p19340143_b_v8_aa.jpg"},{"assetType":"TitleTreatment","assetTypes":["TitleTreatment"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p13641079_ttl_h95_aa.png"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F19340143~~2FSH025886890000","rootId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","shortDescription":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"60","seriesNumber":"4","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fgn.tv~~2F19340143~~2FSH025886890000","rootId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1648142400000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21764379|en-GB"}' - ) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-16T23:30:00.000Z', - stop: '2022-03-17T02:30:00.000Z', - title: 'Live: NHL Hockey', - description: - 'The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.', - category: ['Sport', 'Ice Hockey'] - }, - { - start: '2022-03-17T02:30:00.000Z', - stop: '2022-03-17T06:00:00.000Z', - title: 'Challenge Cup Ice Hockey', - description: - 'Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.', - category: ['Ice Hockey'] - }, - { - start: '2022-03-17T10:00:00.000Z', - stop: '2022-03-17T12:00:00.000Z', - title: 'NHL Hockey', - description: - 'The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.', - category: ['Sport', 'Ice Hockey'] - }, - { - start: '2022-03-17T17:45:00.000Z', - stop: '2022-03-17T18:15:00.000Z', - title: 'Boxing World Weekly', - description: - 'A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.', - category: ['Sport', 'Boxing'], - season: '4', - episode: '60' - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', - channel, - date - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run channels:parse -- --config=./sites/virginmedia.com/virginmedia.com.config.js --output=./sites/virginmedia.com/virginmedia.com.channels.xml +// npm run grab -- --site=virginmedia.com + +const { parser, url } = require('./virginmedia.com.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-03-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1761', + xmltv_id: 'ViaplaySports1.uk' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/1' + ) +}) + +it('can parse response', done => { + const content = + '{"entryCount":410,"totalResults":410,"updated":1647459686755,"expires":1647460298218,"title":"EPG","periods":4,"periodStartTime":1647475200000,"periodEndTime":1647496800000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","t":"Live: NHL Hockey","s":1647473400000,"e":1647484200000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + + axios.get.mockImplementation(url => { + if ( + url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/2' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":410,"totalResults":410,"updated":1647460887411,"expires":1647461895572,"title":"EPG","periods":4,"periodStartTime":1647496800000,"periodEndTime":1647518400000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","t":"Challenge Cup Ice Hockey","s":1647484200000,"e":1647496800000,"c":"lgi-gb-prodobo-master:genre-123","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/3' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":410,"totalResults":410,"updated":1647460871713,"expires":1647461910282,"title":"EPG","periods":4,"periodStartTime":1647518400000,"periodEndTime":1647540000000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","t":"NHL Hockey","s":1647511200000,"e":1647518400000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/4' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":410,"totalResults":410,"updated":1647460871713,"expires":1647461920720,"title":"EPG","periods":4,"periodStartTime":1647540000000,"periodEndTime":1647561600000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","t":"Boxing World Weekly","s":1647539100000,"e":1647540900000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","startTime":1647473400000,"endTime":1647484200000,"actualStartTime":1647473400000,"actualEndTime":1647484200000,"expirationDate":1648078200000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:de610af9a9b049c8a0245173f273136d36458f6f","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005","title":"Live: NHL Hockey","description":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","longDescription":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResLandscapeProductionStill","assetTypes":["HighResLandscapeProductionStill"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p21763419_tb2_h8_aa.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p21763419_tb2_v12_aa.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH013520120000","rootId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","shortDescription":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"2022031605","seriesNumber":"20120000","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"Boston Bruins at Minnesota Wild"},"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH013520120000","rootId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647484200000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21763419|en-GB"}' + ) + }) + } else if ( + url === + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","startTime":1647484200000,"endTime":1647496800000,"actualStartTime":1647484200000,"actualEndTime":1647496800000,"expirationDate":1648089000000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005","title":"Challenge Cup Ice Hockey","description":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","longDescription":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p11743980_b_v12_aa.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH021779870000","rootId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","shortDescription":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"2022031605","seriesNumber":"79870000","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"Final: Belfast Giants v Cardiff Devils"},"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH021779870000","rootId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647928800000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21720572|en-GB"}' + ) + }) + } else if ( + url === + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","startTime":1647511200000,"endTime":1647518400000,"actualStartTime":1647511200000,"actualEndTime":1647518400000,"expirationDate":1648116000000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435","title":"NHL Hockey","description":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","longDescription":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResLandscapeProductionStill","assetTypes":["HighResLandscapeProductionStill"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p448880_b_h8_ak.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p448880_b_v12_ak.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F21275201~~2FSH012830210000","rootId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","shortDescription":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"194","seriesNumber":"102022","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"New Jersey Devils at Calgary Flames"},"parentId":"crid:~~2F~~2Fgn.tv~~2F21275201~~2FSH012830210000","rootId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647583200000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21763550|en-GB"}' + ) + }) + } else if ( + url === + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","startTime":1647539100000,"endTime":1647540900000,"actualStartTime":1647539100000,"actualEndTime":1647540900000,"expirationDate":1648143900000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:c02da14358110cec07d14dc154717ce62ba2f489","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145","title":"Boxing World Weekly","description":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","longDescription":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-83","title":"Boxing","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p19340143_b_v8_aa.jpg"},{"assetType":"TitleTreatment","assetTypes":["TitleTreatment"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p13641079_ttl_h95_aa.png"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F19340143~~2FSH025886890000","rootId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","shortDescription":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"60","seriesNumber":"4","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fgn.tv~~2F19340143~~2FSH025886890000","rootId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1648142400000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21764379|en-GB"}' + ) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-16T23:30:00.000Z', + stop: '2022-03-17T02:30:00.000Z', + title: 'Live: NHL Hockey', + description: + 'The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.', + category: ['Sport', 'Ice Hockey'] + }, + { + start: '2022-03-17T02:30:00.000Z', + stop: '2022-03-17T06:00:00.000Z', + title: 'Challenge Cup Ice Hockey', + description: + 'Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.', + category: ['Ice Hockey'] + }, + { + start: '2022-03-17T10:00:00.000Z', + stop: '2022-03-17T12:00:00.000Z', + title: 'NHL Hockey', + description: + 'The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.', + category: ['Sport', 'Ice Hockey'] + }, + { + start: '2022-03-17T17:45:00.000Z', + stop: '2022-03-17T18:15:00.000Z', + title: 'Boxing World Weekly', + description: + 'A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.', + category: ['Sport', 'Boxing'], + season: '4', + episode: '60' + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', + channel, + date + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js b/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js index b1fa61440..23c8c34c5 100644 --- a/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js +++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js @@ -1,82 +1,82 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'virginmediatelevision.ie', - days: 2, - url({ date }) { - return `https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=${date.format( - 'YYYY-MM-DD' - )}` - }, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - let duration = parseDuration($item) - let stop = start.add(duration, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - sub_title: parseSubTitle($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.info > h2').text().trim() -} - -function parseDescription($item) { - return $item('.info').data('description') -} - -function parseSubTitle($item) { - return $item('.info').data('subtitle') -} - -function parseIcon($item) { - return $item('.info').data('image') -} - -function parseStart($item, date) { - const [time] = $item('.info') - .data('time') - .match(/^\d{1,2}\.\d{2}(am|pm)/) || [null] - - if (!time) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h.mma', 'Europe/London') -} - -function parseDuration($item) { - const duration = $item('.info > .time').data('minutes') - - return duration ? parseInt(duration) : 30 -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - - return $(`.programs_parent > .programs[data-channel='${channel.site_id}'] > .program`).toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'virginmediatelevision.ie', + days: 2, + url({ date }) { + return `https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=${date.format( + 'YYYY-MM-DD' + )}` + }, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + let duration = parseDuration($item) + let stop = start.add(duration, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + sub_title: parseSubTitle($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.info > h2').text().trim() +} + +function parseDescription($item) { + return $item('.info').data('description') +} + +function parseSubTitle($item) { + return $item('.info').data('subtitle') +} + +function parseIcon($item) { + return $item('.info').data('image') +} + +function parseStart($item, date) { + const [time] = $item('.info') + .data('time') + .match(/^\d{1,2}\.\d{2}(am|pm)/) || [null] + + if (!time) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h.mma', 'Europe/London') +} + +function parseDuration($item) { + const duration = $item('.info > .time').data('minutes') + + return duration ? parseInt(duration) : 30 +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + + return $(`.programs_parent > .programs[data-channel='${channel.site_id}'] > .program`).toArray() +} diff --git a/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js b/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js index 9f81f2ff9..f18bd382a 100644 --- a/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js +++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js @@ -1,53 +1,53 @@ -// npm run grab -- --site=virginmediatelevision.ie - -const { parser, url } = require('./virginmediatelevision.ie.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-31', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'one', - xmltv_id: 'VirginMediaOne.ie' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=2023-01-31' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(23) - expect(results[0]).toMatchObject({ - start: '2023-01-31T00:00:00.000Z', - stop: '2023-01-31T01:00:00.000Z', - title: 'Chasing Shadows', - sub_title: '', - description: - 'A detective sergeant and expert in the field of serial killers working for the Missing Persons Bureau tries to protect the general public from evil.', - icon: 'https://bcboltvirgin.akamaized.net/player/shows/1498_517x291_1528141264.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - channel, - content: '' - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=virginmediatelevision.ie + +const { parser, url } = require('./virginmediatelevision.ie.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-31', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'one', + xmltv_id: 'VirginMediaOne.ie' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=2023-01-31' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(23) + expect(results[0]).toMatchObject({ + start: '2023-01-31T00:00:00.000Z', + stop: '2023-01-31T01:00:00.000Z', + title: 'Chasing Shadows', + sub_title: '', + description: + 'A detective sergeant and expert in the field of serial killers working for the Missing Persons Bureau tries to protect the general public from evil.', + icon: 'https://bcboltvirgin.akamaized.net/player/shows/1498_517x291_1528141264.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + channel, + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/visionplus.id/visionplus.id.config.js b/sites/visionplus.id/visionplus.id.config.js index c1a880303..ddc68d78a 100644 --- a/sites/visionplus.id/visionplus.id.config.js +++ b/sites/visionplus.id/visionplus.id.config.js @@ -1,91 +1,91 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const cheerio = require('cheerio') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'visionplus.id', - days: 2, - request: { - headers: { - Authorization: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5NDY0NTE4OTcsInVpZCI6MCwicGwiOiJ3ZWIiLCJndWVzdF90b2tlbiI6ImNhNGNjMjdiNzc3MjBjODEwNzQ2YzY3MTY4NzNjMDI3NGU4NWYxMWQifQ.tt08jLZ3HiNadUeSgc9O-nhIzEi7WMYRjxMb05lEZ74' - } - }, - url({ date, channel }) { - return `https://epg-api.visionplus.id/api/v1/epg?isLive=false&start_time_from=${date.format( - 'YYYY-MM-DD' - )}&channel_ids=${channel.site_id}` - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - let prev = programs[programs.length - 1] - let start = parseStart(item, date) - let stop = parseStop(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - } - if (stop.isBefore(prev.stop)) { - stop = stop.add(1, 'd') - } - } - - programs.push({ - title: item.t, - description: item.synopsis, - start, - stop - }) - }) - - return programs - }, - async channels() { - const xml = await axios - .get('https://www.visionplus.id/sitemap-channels.xml') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(xml) - const items = $('url').toArray() - - return items.map(item => { - const $item = cheerio.load(item) - const loc = $item('loc').text() - const [, site_id] = loc.match(/channel\/(\d+)\//) || [null, null] - - return { - site_id, - name: $item('video\\:title').text().trim() - } - }) - } -} - -function parseStart(item, date) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.s}`, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') -} - -function parseStop(item, date) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.e}`, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') -} - -function parseItems(content, channel, date) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data)) return [] - const channelData = data.data.find(c => c.id === channel.site_id) - if (!channelData || !Array.isArray(channelData.schedules)) return [] - const daySchedule = channelData.schedules.find(d => d.date === date.format('YYYY-MM-DD')) - if (!daySchedule || !Array.isArray(daySchedule.items)) return [] - - return daySchedule.items -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const cheerio = require('cheerio') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'visionplus.id', + days: 2, + request: { + headers: { + Authorization: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5NDY0NTE4OTcsInVpZCI6MCwicGwiOiJ3ZWIiLCJndWVzdF90b2tlbiI6ImNhNGNjMjdiNzc3MjBjODEwNzQ2YzY3MTY4NzNjMDI3NGU4NWYxMWQifQ.tt08jLZ3HiNadUeSgc9O-nhIzEi7WMYRjxMb05lEZ74' + } + }, + url({ date, channel }) { + return `https://epg-api.visionplus.id/api/v1/epg?isLive=false&start_time_from=${date.format( + 'YYYY-MM-DD' + )}&channel_ids=${channel.site_id}` + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + let prev = programs[programs.length - 1] + let start = parseStart(item, date) + let stop = parseStop(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + } + if (stop.isBefore(prev.stop)) { + stop = stop.add(1, 'd') + } + } + + programs.push({ + title: item.t, + description: item.synopsis, + start, + stop + }) + }) + + return programs + }, + async channels() { + const xml = await axios + .get('https://www.visionplus.id/sitemap-channels.xml') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(xml) + const items = $('url').toArray() + + return items.map(item => { + const $item = cheerio.load(item) + const loc = $item('loc').text() + const [, site_id] = loc.match(/channel\/(\d+)\//) || [null, null] + + return { + site_id, + name: $item('video\\:title').text().trim() + } + }) + } +} + +function parseStart(item, date) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.s}`, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') +} + +function parseStop(item, date) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.e}`, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') +} + +function parseItems(content, channel, date) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data)) return [] + const channelData = data.data.find(c => c.id === channel.site_id) + if (!channelData || !Array.isArray(channelData.schedules)) return [] + const daySchedule = channelData.schedules.find(d => d.date === date.format('YYYY-MM-DD')) + if (!daySchedule || !Array.isArray(daySchedule.items)) return [] + + return daySchedule.items +} diff --git a/sites/visionplus.id/visionplus.id.test.js b/sites/visionplus.id/visionplus.id.test.js index 430ecbf37..881db82b1 100644 --- a/sites/visionplus.id/visionplus.id.test.js +++ b/sites/visionplus.id/visionplus.id.test.js @@ -1,64 +1,64 @@ -// npm run channels:parse -- --config=./sites/visionplus.id/visionplus.id.config.js --output=./sites/visionplus.id/visionplus.id.channels.xml -// npm run grab -- --site=visionplus.id - -const { parser, url, request } = require('./visionplus.id.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-06-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'RCTI.id' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epg-api.visionplus.id/api/v1/epg?isLive=false&start_time_from=2023-06-30&channel_ids=2' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Authorization: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5NDY0NTE4OTcsInVpZCI6MCwicGwiOiJ3ZWIiLCJndWVzdF90b2tlbiI6ImNhNGNjMjdiNzc3MjBjODEwNzQ2YzY3MTY4NzNjMDI3NGU4NWYxMWQifQ.tt08jLZ3HiNadUeSgc9O-nhIzEi7WMYRjxMb05lEZ74' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(18) - expect(results[0]).toMatchObject({ - start: '2023-06-29T18:15:00.000Z', - stop: '2023-06-29T19:00:00.000Z', - title: 'Hafalan Shalat Delisa', - description: '' - }) - - expect(results[17]).toMatchObject({ - start: '2023-06-30T16:15:00.000Z', - stop: '2023-06-30T18:15:00.000Z', - title: 'Tukang Bubur Pulang Haji', - description: '' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ content, channel }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/visionplus.id/visionplus.id.config.js --output=./sites/visionplus.id/visionplus.id.channels.xml +// npm run grab -- --site=visionplus.id + +const { parser, url, request } = require('./visionplus.id.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-06-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'RCTI.id' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epg-api.visionplus.id/api/v1/epg?isLive=false&start_time_from=2023-06-30&channel_ids=2' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Authorization: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5NDY0NTE4OTcsInVpZCI6MCwicGwiOiJ3ZWIiLCJndWVzdF90b2tlbiI6ImNhNGNjMjdiNzc3MjBjODEwNzQ2YzY3MTY4NzNjMDI3NGU4NWYxMWQifQ.tt08jLZ3HiNadUeSgc9O-nhIzEi7WMYRjxMb05lEZ74' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(18) + expect(results[0]).toMatchObject({ + start: '2023-06-29T18:15:00.000Z', + stop: '2023-06-29T19:00:00.000Z', + title: 'Hafalan Shalat Delisa', + description: '' + }) + + expect(results[17]).toMatchObject({ + start: '2023-06-30T16:15:00.000Z', + stop: '2023-06-30T18:15:00.000Z', + title: 'Tukang Bubur Pulang Haji', + description: '' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content, channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/vivacom.bg/vivacom.bg.config.js b/sites/vivacom.bg/vivacom.bg.config.js index eaf37a22c..53ea4edfa 100644 --- a/sites/vivacom.bg/vivacom.bg.config.js +++ b/sites/vivacom.bg/vivacom.bg.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'vivacom.bg', - days: 2, - url({ date, channel }) { - const [page] = channel.site_id.split('#') - - return `https://www.vivacom.bg/bg/tv/programa/?date=${date.format('YYYY-MM-DD')}&page=${page}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - start: parseStart(item, date), - stop: parseStop(item, date) - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const $ = cheerio.load(item) - let [, time] = $('span') - .text() - .match(/^(\d{2}:\d{2}:\d{2})/) || [null, null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Sofia').toJSON() -} - -function parseStop(item, date) { - const $ = cheerio.load(item) - let [, time] = $('span') - .text() - .match(/(\d{2}:\d{2}:\d{2})$/) || [null, null] - if (!time) return null - if (time === '00:00:00') date = date.add(1, 'd') - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Sofia').toJSON() -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('h3').text() -} - -function parseDescription(item) { - const $ = cheerio.load(item) - - return $('p').text() -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const $ = cheerio.load(content) - const listItem = $(`#scroll-vertical > li[title="${channelId}"]`) - const i = $('#scroll-vertical > li').index(listItem) - - return $(`#scroll-horizontal > ul:nth-child(${i + 1}) li`).toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'vivacom.bg', + days: 2, + url({ date, channel }) { + const [page] = channel.site_id.split('#') + + return `https://www.vivacom.bg/bg/tv/programa/?date=${date.format('YYYY-MM-DD')}&page=${page}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + start: parseStart(item, date), + stop: parseStop(item, date) + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const $ = cheerio.load(item) + let [, time] = $('span') + .text() + .match(/^(\d{2}:\d{2}:\d{2})/) || [null, null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Sofia').toJSON() +} + +function parseStop(item, date) { + const $ = cheerio.load(item) + let [, time] = $('span') + .text() + .match(/(\d{2}:\d{2}:\d{2})$/) || [null, null] + if (!time) return null + if (time === '00:00:00') date = date.add(1, 'd') + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Sofia').toJSON() +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('h3').text() +} + +function parseDescription(item) { + const $ = cheerio.load(item) + + return $('p').text() +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const $ = cheerio.load(content) + const listItem = $(`#scroll-vertical > li[title="${channelId}"]`) + const i = $('#scroll-vertical > li').index(listItem) + + return $(`#scroll-horizontal > ul:nth-child(${i + 1}) li`).toArray() +} diff --git a/sites/vivacom.bg/vivacom.bg.test.js b/sites/vivacom.bg/vivacom.bg.test.js index c47f18106..44b9e1d9c 100644 --- a/sites/vivacom.bg/vivacom.bg.test.js +++ b/sites/vivacom.bg/vivacom.bg.test.js @@ -1,46 +1,46 @@ -// npm run grab -- --site=vivacom.bg - -const { parser, url } = require('./vivacom.bg.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '1#БНТ 1 HD', xmltv_id: 'BNT1.bg' } -const content = - '
        • Когато сърцето зове

          04:25:00 - 05:10:00

          Телевизионен филм. Тв филм /4 сезон, 5 епизод/п/

        • Dreamy Nights: Songs & Rhymes

          23:30:00 - 00:00:00

          Songs & Rhymes, Flowers, Milky Way, Close Your Eyes, Twilight

        ' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://www.vivacom.bg/bg/tv/programa/?date=2021-11-05&page=1') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-05T02:25:00.000Z', - stop: '2021-11-05T03:10:00.000Z', - title: 'Когато сърцето зове', - description: 'Телевизионен филм. Тв филм /4 сезон, 5 епизод/п/' - }, - { - start: '2021-11-05T21:30:00.000Z', - stop: '2021-11-05T22:00:00.000Z', - title: 'Dreamy Nights: Songs & Rhymes', - description: 'Songs & Rhymes, Flowers, Milky Way, Close Your Eyes, Twilight' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '
        ' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=vivacom.bg + +const { parser, url } = require('./vivacom.bg.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '1#БНТ 1 HD', xmltv_id: 'BNT1.bg' } +const content = + '
        • Когато сърцето зове

          04:25:00 - 05:10:00

          Телевизионен филм. Тв филм /4 сезон, 5 епизод/п/

        • Dreamy Nights: Songs & Rhymes

          23:30:00 - 00:00:00

          Songs & Rhymes, Flowers, Milky Way, Close Your Eyes, Twilight

        ' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://www.vivacom.bg/bg/tv/programa/?date=2021-11-05&page=1') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-05T02:25:00.000Z', + stop: '2021-11-05T03:10:00.000Z', + title: 'Когато сърцето зове', + description: 'Телевизионен филм. Тв филм /4 сезон, 5 епизод/п/' + }, + { + start: '2021-11-05T21:30:00.000Z', + stop: '2021-11-05T22:00:00.000Z', + title: 'Dreamy Nights: Songs & Rhymes', + description: 'Songs & Rhymes, Flowers, Milky Way, Close Your Eyes, Twilight' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '
        ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/vtm.be/vtm.be.config.js b/sites/vtm.be/vtm.be.config.js index 7dbe8b218..21fddf2f2 100644 --- a/sites/vtm.be/vtm.be.config.js +++ b/sites/vtm.be/vtm.be.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') -const isBetween = require('dayjs/plugin/isBetween') - -dayjs.extend(isBetween) - -module.exports = { - site: 'vtm.be', - days: 2, - url: function ({ channel }) { - return `https://vtm.be/tv-gids/${channel.site_id}` - }, - request: { - headers: { - Cookie: - 'ak_bmsc=8103DDA2C2C37ECD922124463C746A4C~000000000000000000000000000000~YAAQNwVJF7ndI+p8AQAAYDkcCg0mQAkQ2jDHjSfnXl9VIGnzditECZ1FDj1Yi72a8rv/Q454lDDY0Dm3TPqxJUuNLzxJGmgkLmei4IIIwzKJWbB6wC/FMQApoI1NbGz+tUErryic1HWdbZ2dz1IX+AkOHJ9RVupYG5GmkSEQdFG1+/dSZoBMWEeb/5VOCLmNXRDP7k8LnSXaIuKqp5c2MQB+uQ9DdHUd6bIje3dzuxbka9+nJZ+eX/pNbgWI41X2tiXLvPZKh91Tk9k98zrK0pwBnGpTJqDVxmafYH/CjkXoLgEUW3loZfgL9SqddG706a4LnRPhyLzW6W6SH7Q0QOFE4g54NKADVttS2gbXgVrICvo0bb0FAESaFjc5uDyOd+fV2XBGzw==; authId=54da9bc2-d387-4923-8773-3d33ec68710e; gtm_session=1; _sp_ses.417f=*; _ga=GA1.2.525677035.1636552212; _gid=GA1.2.386833723.1636552212; tcf20_purposes=functional|analytics|targeted_advertising|non-personalised_ads|personalisation|marketing|social_media|advertising_1|advertising_2|advertising_3|advertising_4|advertising_7|advertising_9|advertising_10; _gcl_au=1.1.112810754.1636552212; _gat_UA-538372-57=1; sp=4a32f074-5526-4654-9389-2516d799ec68; _gat_UA-6602938-21=1; _sp_id.417f=0c81a857-09dc-47c2-8e51-4fed976211c4.1636552212.1.1636552214.1636552212.55934f90-4bad-47ff-8c5e-cf904126dcfb; bm_sv=1A45EF31D80D05B688C17EAD85964E29~hFpINNxpFphfJ2LLPoLQTauvUpyAf3kaTeGZAMfI/UTMlTRFjoAGBQJPEUPvSw3rXw/swqqAICc74l56pEBVSw6aJYqaoRaiRAZXyWZzQ6jAoeP5SMsZwtvNzYQ3aJXVWM8W8a98J0trlnSjIIsRPQ==' - } - }, - parser: function ({ content, date }) { - let programs = [] - const data = parseContent(content) - const items = parseItems(data, date) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.synopsis, - category: item.genre, - icon: item.imageUrl, - start: dayjs(item.from).toJSON(), - stop: dayjs(item.to).toJSON() - }) - }) - - return programs - } -} - -function parseContent(content) { - const [, json] = content.match(/window.__EPG_REDUX_DATA__=(.*);\n/i) || [null, null] - const data = JSON.parse(json) - - return data -} - -function parseItems(data, date) { - if (!data || !data.broadcasts) return [] - - return Object.values(data.broadcasts).filter(i => dayjs(i.from).isBetween(date, date.add(1, 'd'))) -} +const dayjs = require('dayjs') +const isBetween = require('dayjs/plugin/isBetween') + +dayjs.extend(isBetween) + +module.exports = { + site: 'vtm.be', + days: 2, + url: function ({ channel }) { + return `https://vtm.be/tv-gids/${channel.site_id}` + }, + request: { + headers: { + Cookie: + 'ak_bmsc=8103DDA2C2C37ECD922124463C746A4C~000000000000000000000000000000~YAAQNwVJF7ndI+p8AQAAYDkcCg0mQAkQ2jDHjSfnXl9VIGnzditECZ1FDj1Yi72a8rv/Q454lDDY0Dm3TPqxJUuNLzxJGmgkLmei4IIIwzKJWbB6wC/FMQApoI1NbGz+tUErryic1HWdbZ2dz1IX+AkOHJ9RVupYG5GmkSEQdFG1+/dSZoBMWEeb/5VOCLmNXRDP7k8LnSXaIuKqp5c2MQB+uQ9DdHUd6bIje3dzuxbka9+nJZ+eX/pNbgWI41X2tiXLvPZKh91Tk9k98zrK0pwBnGpTJqDVxmafYH/CjkXoLgEUW3loZfgL9SqddG706a4LnRPhyLzW6W6SH7Q0QOFE4g54NKADVttS2gbXgVrICvo0bb0FAESaFjc5uDyOd+fV2XBGzw==; authId=54da9bc2-d387-4923-8773-3d33ec68710e; gtm_session=1; _sp_ses.417f=*; _ga=GA1.2.525677035.1636552212; _gid=GA1.2.386833723.1636552212; tcf20_purposes=functional|analytics|targeted_advertising|non-personalised_ads|personalisation|marketing|social_media|advertising_1|advertising_2|advertising_3|advertising_4|advertising_7|advertising_9|advertising_10; _gcl_au=1.1.112810754.1636552212; _gat_UA-538372-57=1; sp=4a32f074-5526-4654-9389-2516d799ec68; _gat_UA-6602938-21=1; _sp_id.417f=0c81a857-09dc-47c2-8e51-4fed976211c4.1636552212.1.1636552214.1636552212.55934f90-4bad-47ff-8c5e-cf904126dcfb; bm_sv=1A45EF31D80D05B688C17EAD85964E29~hFpINNxpFphfJ2LLPoLQTauvUpyAf3kaTeGZAMfI/UTMlTRFjoAGBQJPEUPvSw3rXw/swqqAICc74l56pEBVSw6aJYqaoRaiRAZXyWZzQ6jAoeP5SMsZwtvNzYQ3aJXVWM8W8a98J0trlnSjIIsRPQ==' + } + }, + parser: function ({ content, date }) { + let programs = [] + const data = parseContent(content) + const items = parseItems(data, date) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.synopsis, + category: item.genre, + icon: item.imageUrl, + start: dayjs(item.from).toJSON(), + stop: dayjs(item.to).toJSON() + }) + }) + + return programs + } +} + +function parseContent(content) { + const [, json] = content.match(/window.__EPG_REDUX_DATA__=(.*);\n/i) || [null, null] + const data = JSON.parse(json) + + return data +} + +function parseItems(data, date) { + if (!data || !data.broadcasts) return [] + + return Object.values(data.broadcasts).filter(i => dayjs(i.from).isBetween(date, date.add(1, 'd'))) +} diff --git a/sites/vtm.be/vtm.be.test.js b/sites/vtm.be/vtm.be.test.js index 178f7f643..172eaea9d 100644 --- a/sites/vtm.be/vtm.be.test.js +++ b/sites/vtm.be/vtm.be.test.js @@ -1,45 +1,45 @@ -// npm run grab -- --site=vtm.be - -const { parser, url } = require('./vtm.be.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'vtm', - xmltv_id: 'VTM.be' -} -const content = ` ` - -it('can generate valid url', () => { - const result = url({ channel }) - expect(result).toBe('https://vtm.be/tv-gids/vtm') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-10T23:45:00.000Z', - stop: '2021-11-11T00:20:00.000Z', - title: 'Wooninspiraties', - icon: 'https://images4.persgroep.net/rcs/z5qrZHumkjuN5rWzoaRJ_BTdL7A/diocontent/209688322/_fill/600/400?appId=da11c75db9b73ea0f41f0cd0da631c71', - description: - 'Een team gaat op pad om inspiratie op te doen over alles wat met wonen en leven te maken heeft; Ze trekken heel het land door om de laatste trends en tips op het gebied van wonen te achterhalen.', - category: 'Magazine' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=vtm.be + +const { parser, url } = require('./vtm.be.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'vtm', + xmltv_id: 'VTM.be' +} +const content = ` ` + +it('can generate valid url', () => { + const result = url({ channel }) + expect(result).toBe('https://vtm.be/tv-gids/vtm') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-10T23:45:00.000Z', + stop: '2021-11-11T00:20:00.000Z', + title: 'Wooninspiraties', + icon: 'https://images4.persgroep.net/rcs/z5qrZHumkjuN5rWzoaRJ_BTdL7A/diocontent/209688322/_fill/600/400?appId=da11c75db9b73ea0f41f0cd0da631c71', + description: + 'Een team gaat op pad om inspiratie op te doen over alles wat met wonen en leven te maken heeft; Ze trekken heel het land door om de laatste trends en tips op het gebied van wonen te achterhalen.', + category: 'Magazine' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ' ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/walesi.com.fj/walesi.com.fj.config.js b/sites/walesi.com.fj/walesi.com.fj.config.js index 4b715edce..156582ad4 100644 --- a/sites/walesi.com.fj/walesi.com.fj.config.js +++ b/sites/walesi.com.fj/walesi.com.fj.config.js @@ -1,91 +1,91 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'walesi.com.fj', - days: 2, - url: 'https://www.walesi.com.fj/wp-admin/admin-ajax.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('chanel', channel.site_id) - params.append('date', date.unix()) - params.append('action', 'extvs_get_schedule_simple') - - return params - } - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const start = parseStart($item, date) - const stop = start.add(30, 'm') - if (prev) prev.stop = start - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.walesi.com.fj/channel-guide/') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - const channels = $( - 'div.ex-chanel-list > div.extvs-inline-chanel > ul > li.extvs-inline-select' - ).toArray() - return channels.map(item => { - const $item = cheerio.load(item) - const [, name] = $item('span') - .text() - .trim() - .match(/\d+\. (.*)/) || [null, null] - return { - lang: 'fj', - site_id: $item('*').data('value'), - name - } - }) - } -} - -function parseTitle($item) { - return $item('td.extvs-table1-programme > div > div > figure > h3').text() -} - -function parseStart($item, date) { - let time = $item('td.extvs-table1-time > span').text().trim() - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD H:mm a', 'Pacific/Fiji') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data.html) return [] - const $ = cheerio.load(data.html) - - return $('table > tbody > tr').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'walesi.com.fj', + days: 2, + url: 'https://www.walesi.com.fj/wp-admin/admin-ajax.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('chanel', channel.site_id) + params.append('date', date.unix()) + params.append('action', 'extvs_get_schedule_simple') + + return params + } + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const start = parseStart($item, date) + const stop = start.add(30, 'm') + if (prev) prev.stop = start + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.walesi.com.fj/channel-guide/') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + const channels = $( + 'div.ex-chanel-list > div.extvs-inline-chanel > ul > li.extvs-inline-select' + ).toArray() + return channels.map(item => { + const $item = cheerio.load(item) + const [, name] = $item('span') + .text() + .trim() + .match(/\d+\. (.*)/) || [null, null] + return { + lang: 'fj', + site_id: $item('*').data('value'), + name + } + }) + } +} + +function parseTitle($item) { + return $item('td.extvs-table1-programme > div > div > figure > h3').text() +} + +function parseStart($item, date) { + let time = $item('td.extvs-table1-time > span').text().trim() + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD H:mm a', 'Pacific/Fiji') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data.html) return [] + const $ = cheerio.load(data.html) + + return $('table > tbody > tr').toArray() +} diff --git a/sites/walesi.com.fj/walesi.com.fj.test.js b/sites/walesi.com.fj/walesi.com.fj.test.js index 05d834eb7..30068848f 100644 --- a/sites/walesi.com.fj/walesi.com.fj.test.js +++ b/sites/walesi.com.fj/walesi.com.fj.test.js @@ -1,68 +1,68 @@ -// npm run channels:parse -- --config=./sites/walesi.com.fj/walesi.com.fj.config.js --output=./sites/walesi.com.fj/walesi.com.fj.channels.xml -// npm run grab -- --site=walesi.com.fj - -const { parser, url, request } = require('./walesi.com.fj.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'fbc-2', - xmltv_id: 'FBCTV.fj' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.walesi.com.fj/wp-admin/admin-ajax.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date, channel }) - expect(result.has('chanel')).toBe(true) - expect(result.has('date')).toBe(true) - expect(result.has('action')).toBe(true) -}) - -it('can parse response', () => { - const content = - '{"html":"\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\r\\n\\t\\r\\n\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n
        ImageTimeProgramme
        \\r\\n\\t\\t\\t12:00 am\\r\\n\\t\\t
        \\r\\n\\t\\t\\t
        \\r\\n\\t\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t

        Aljazeera

        \\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t
        \\r\\n\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t
        \\r\\n\\t
        6:00 am

        Move Fiji

        \\r\\n\\t\\t\\t\\t"}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-20T12:00:00.000Z', - stop: '2021-11-20T18:00:00.000Z', - title: 'Aljazeera' - }, - { - start: '2021-11-20T18:00:00.000Z', - stop: '2021-11-20T18:30:00.000Z', - title: 'Move Fiji' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"html":"

        No matching records found

        "}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/walesi.com.fj/walesi.com.fj.config.js --output=./sites/walesi.com.fj/walesi.com.fj.channels.xml +// npm run grab -- --site=walesi.com.fj + +const { parser, url, request } = require('./walesi.com.fj.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'fbc-2', + xmltv_id: 'FBCTV.fj' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.walesi.com.fj/wp-admin/admin-ajax.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date, channel }) + expect(result.has('chanel')).toBe(true) + expect(result.has('date')).toBe(true) + expect(result.has('action')).toBe(true) +}) + +it('can parse response', () => { + const content = + '{"html":"\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\r\\n\\t\\r\\n\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n
        ImageTimeProgramme
        \\r\\n\\t\\t\\t12:00 am\\r\\n\\t\\t
        \\r\\n\\t\\t\\t
        \\r\\n\\t\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t

        Aljazeera

        \\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t
        \\r\\n\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t
        \\r\\n\\t
        6:00 am

        Move Fiji

        \\r\\n\\t\\t\\t\\t"}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-20T12:00:00.000Z', + stop: '2021-11-20T18:00:00.000Z', + title: 'Aljazeera' + }, + { + start: '2021-11-20T18:00:00.000Z', + stop: '2021-11-20T18:30:00.000Z', + title: 'Move Fiji' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"html":"

        No matching records found

        "}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/watchyour.tv/watchyour.tv.config.js b/sites/watchyour.tv/watchyour.tv.config.js index 316ccfb34..19e4d64bd 100644 --- a/sites/watchyour.tv/watchyour.tv.config.js +++ b/sites/watchyour.tv/watchyour.tv.config.js @@ -1,55 +1,55 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -module.exports = { - site: 'watchyour.tv', - days: 2, - url: 'https://www.watchyour.tv/guide.json', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, date, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(parseInt(item.duration), 'm') - programs.push({ - title: item.name, - icon: item.icon, - category: item.category, - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.watchyour.tv/guide.json') - .then(r => r.data) - .catch(console.log) - - return data.map(item => ({ - site_id: item.id, - name: item.name - })) - } -} - -function parseStart(item) { - return dayjs.unix(parseInt(item.tms)) -} - -function parseItems(content, date, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - const channelData = data.find(i => i.id == channel.site_id) - if (!channelData || !Array.isArray(channelData.shows)) return [] - - return channelData.shows.filter(i => i.start_day === date.format('YYYY-MM-DD')) -} +const dayjs = require('dayjs') +const axios = require('axios') + +module.exports = { + site: 'watchyour.tv', + days: 2, + url: 'https://www.watchyour.tv/guide.json', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, date, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(parseInt(item.duration), 'm') + programs.push({ + title: item.name, + icon: item.icon, + category: item.category, + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.watchyour.tv/guide.json') + .then(r => r.data) + .catch(console.log) + + return data.map(item => ({ + site_id: item.id, + name: item.name + })) + } +} + +function parseStart(item) { + return dayjs.unix(parseInt(item.tms)) +} + +function parseItems(content, date, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + const channelData = data.find(i => i.id == channel.site_id) + if (!channelData || !Array.isArray(channelData.shows)) return [] + + return channelData.shows.filter(i => i.start_day === date.format('YYYY-MM-DD')) +} diff --git a/sites/watchyour.tv/watchyour.tv.test.js b/sites/watchyour.tv/watchyour.tv.test.js index c920892c5..a5e1a170d 100644 --- a/sites/watchyour.tv/watchyour.tv.test.js +++ b/sites/watchyour.tv/watchyour.tv.test.js @@ -1,48 +1,48 @@ -// npm run channels:parse -- --config=./sites/watchyour.tv/watchyour.tv.config.js --output=./sites/watchyour.tv/watchyour.tv.channels.xml -// npm run grab -- --site=watchyour.tv - -const { parser, url } = require('./watchyour.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '735', - xmltv_id: 'TVSClassicSports.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.watchyour.tv/guide.json') -}) - -it('can parse response', () => { - const content = - '[{"name":"TVS Classic Sports","icon":"https://www.watchyour.tv/epg/channellogos/tvs-classic-sports.png","language":"English","id":"735","shows":[{"name":"1979 WVU vs Penn State","category":"Sports","start_day":"2022-10-03","start":"04:00:00","end_day":"2022-10-03","end":"06:00:45","duration":"121","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index-1664769600-7245.m3u8?token=f7410a9414f61579dced17ac1bbdb971","icon":"https://example.com/image.png","timezone":"+0000","tms":"1664769600"},{"name":"1958 NCAA University of Kentucky vs Seattle U","category":"Sports","start_day":"2022-10-04","start":"00:58:50","end_day":"2022-10-04","end":"01:44:11","duration":"46","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index.m3u8?token=93e7b201f544c87296076b73f9d880ae","icon":"","timezone":"+0000","tms":"1664845130"}]}]' - const result = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-03T04:00:00.000Z', - stop: '2022-10-03T06:01:00.000Z', - title: '1979 WVU vs Penn State', - icon: 'https://example.com/image.png', - category: 'Sports' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - date, - channel - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/watchyour.tv/watchyour.tv.config.js --output=./sites/watchyour.tv/watchyour.tv.channels.xml +// npm run grab -- --site=watchyour.tv + +const { parser, url } = require('./watchyour.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '735', + xmltv_id: 'TVSClassicSports.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.watchyour.tv/guide.json') +}) + +it('can parse response', () => { + const content = + '[{"name":"TVS Classic Sports","icon":"https://www.watchyour.tv/epg/channellogos/tvs-classic-sports.png","language":"English","id":"735","shows":[{"name":"1979 WVU vs Penn State","category":"Sports","start_day":"2022-10-03","start":"04:00:00","end_day":"2022-10-03","end":"06:00:45","duration":"121","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index-1664769600-7245.m3u8?token=f7410a9414f61579dced17ac1bbdb971","icon":"https://example.com/image.png","timezone":"+0000","tms":"1664769600"},{"name":"1958 NCAA University of Kentucky vs Seattle U","category":"Sports","start_day":"2022-10-04","start":"00:58:50","end_day":"2022-10-04","end":"01:44:11","duration":"46","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index.m3u8?token=93e7b201f544c87296076b73f9d880ae","icon":"","timezone":"+0000","tms":"1664845130"}]}]' + const result = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-03T04:00:00.000Z', + stop: '2022-10-03T06:01:00.000Z', + title: '1979 WVU vs Penn State', + icon: 'https://example.com/image.png', + category: 'Sports' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + date, + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/wavve.com/wavve.com.config.js b/sites/wavve.com/wavve.com.config.js index 347852ed6..59755129f 100644 --- a/sites/wavve.com/wavve.com.config.js +++ b/sites/wavve.com/wavve.com.config.js @@ -1,62 +1,62 @@ -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'wavve.com', - days: 2, - url: function ({ channel, date }) { - return `https://apis.pooq.co.kr/live/epgs/channels/${ - channel.site_id - }?startdatetime=${date.format('YYYY-MM-DD')}%2000%3A00&enddatetime=${date - .add(1, 'd') - .format('YYYY-MM-DD')}%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = [] - - const data = await axios - .get( - 'https://apis.pooq.co.kr/live/epgs?enddatetime=2022-04-17%2019%3A00&genre=all&limit=500&startdatetime=2022-04-17%2016%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9' - ) - .then(r => r.data) - .catch(console.log) - - data.list.forEach(i => { - channels.push({ - name: i.channelname, - site_id: i.channelid, - lang: 'ko' - }) - }) - - return channels - } -} - -function parseStart(item) { - return DateTime.fromFormat(item.starttime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() -} - -function parseStop(item) { - return DateTime.fromFormat(item.endtime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.list)) return [] - - return data.list -} +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'wavve.com', + days: 2, + url: function ({ channel, date }) { + return `https://apis.pooq.co.kr/live/epgs/channels/${ + channel.site_id + }?startdatetime=${date.format('YYYY-MM-DD')}%2000%3A00&enddatetime=${date + .add(1, 'd') + .format('YYYY-MM-DD')}%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = [] + + const data = await axios + .get( + 'https://apis.pooq.co.kr/live/epgs?enddatetime=2022-04-17%2019%3A00&genre=all&limit=500&startdatetime=2022-04-17%2016%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9' + ) + .then(r => r.data) + .catch(console.log) + + data.list.forEach(i => { + channels.push({ + name: i.channelname, + site_id: i.channelid, + lang: 'ko' + }) + }) + + return channels + } +} + +function parseStart(item) { + return DateTime.fromFormat(item.starttime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() +} + +function parseStop(item) { + return DateTime.fromFormat(item.endtime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.list)) return [] + + return data.list +} diff --git a/sites/wavve.com/wavve.com.test.js b/sites/wavve.com/wavve.com.test.js index 7939ef405..23959642d 100644 --- a/sites/wavve.com/wavve.com.test.js +++ b/sites/wavve.com/wavve.com.test.js @@ -1,46 +1,46 @@ -// npm run channels:parse -- --config=sites/wavve.com/wavve.com.config.js --output=sites/wavve.com/wavve.com.channels.xml -// npm run grab -- --site=wavve.com - -const { parser, url } = require('./wavve.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-04-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'K01', - xmltv_id: 'KBS1TV.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://apis.pooq.co.kr/live/epgs/channels/K01?startdatetime=2022-04-17%2000%3A00&enddatetime=2022-04-18%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500' - ) -}) - -it('can parse response', () => { - const content = - '{"pagecount":"37","count":"37","list":[{"cpid":"C3","channelid":"K01","channelname":"KBS 1TV","channelimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS-1TV-1.jpg","scheduleid":"K01_20220416223000","programid":"","title":"특파원 보고 세계는 지금","image":"wchimg.wavve.com/live/thumbnail/K01.jpg","starttime":"2022-04-16 22:30","endtime":"2022-04-16 23:15","timemachine":"Y","license":"y","livemarks":[],"targetage":"0","tvimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS 1TV-2.png","ispreorder":"n","preorderlink":"n","alarm":"n"}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-04-16T13:30:00.000Z', - stop: '2022-04-16T14:15:00.000Z', - title: '특파원 보고 세계는 지금' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"pagecount":"0","count":"0","list":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/wavve.com/wavve.com.config.js --output=sites/wavve.com/wavve.com.channels.xml +// npm run grab -- --site=wavve.com + +const { parser, url } = require('./wavve.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-04-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'K01', + xmltv_id: 'KBS1TV.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://apis.pooq.co.kr/live/epgs/channels/K01?startdatetime=2022-04-17%2000%3A00&enddatetime=2022-04-18%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500' + ) +}) + +it('can parse response', () => { + const content = + '{"pagecount":"37","count":"37","list":[{"cpid":"C3","channelid":"K01","channelname":"KBS 1TV","channelimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS-1TV-1.jpg","scheduleid":"K01_20220416223000","programid":"","title":"특파원 보고 세계는 지금","image":"wchimg.wavve.com/live/thumbnail/K01.jpg","starttime":"2022-04-16 22:30","endtime":"2022-04-16 23:15","timemachine":"Y","license":"y","livemarks":[],"targetage":"0","tvimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS 1TV-2.png","ispreorder":"n","preorderlink":"n","alarm":"n"}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-04-16T13:30:00.000Z', + stop: '2022-04-16T14:15:00.000Z', + title: '특파원 보고 세계는 지금' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"pagecount":"0","count":"0","list":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js b/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js index 7a89a8155..a1b99fee0 100644 --- a/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js +++ b/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js @@ -1,79 +1,79 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'worldfishingnetwork.com', - days: 2, - url({ date }) { - return `https://www.worldfishingnetwork.com/schedule/77420?day=${date.format('ddd')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - let $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.show-title > h3').text().trim() -} - -function parseSubTitle($item) { - return $item('.show-title').clone().children().remove().end().text().trim() -} - -function parseDescription($item) { - return $item('.show-title > p').text().trim() -} - -function parseIcon($item) { - const url = $item('.show-img > img').attr('src') - - return url ? `https:${url}` : null -} - -function parseStart($item, date) { - const time = $item('.show-time > h2').clone().children().remove().end().text().trim() - const period = $item('.show-time > h2 > span > strong').text().trim() - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${time} ${period}`, - 'YYYY-MM-DD HH:mm A', - 'America/New_York' - ) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.show-item').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'worldfishingnetwork.com', + days: 2, + url({ date }) { + return `https://www.worldfishingnetwork.com/schedule/77420?day=${date.format('ddd')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + let $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.show-title > h3').text().trim() +} + +function parseSubTitle($item) { + return $item('.show-title').clone().children().remove().end().text().trim() +} + +function parseDescription($item) { + return $item('.show-title > p').text().trim() +} + +function parseIcon($item) { + const url = $item('.show-img > img').attr('src') + + return url ? `https:${url}` : null +} + +function parseStart($item, date) { + const time = $item('.show-time > h2').clone().children().remove().end().text().trim() + const period = $item('.show-time > h2 > span > strong').text().trim() + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${time} ${period}`, + 'YYYY-MM-DD HH:mm A', + 'America/New_York' + ) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.show-item').toArray() +} diff --git a/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js b/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js index 0b5b609e9..7677c7ca2 100644 --- a/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js +++ b/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=worldfishingnetwork.com - -const { parser, url } = require('./worldfishingnetwork.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-24', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.worldfishingnetwork.com/schedule/77420?day=Tue') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-24T05:00:00.000Z', - stop: '2023-01-24T07:00:00.000Z', - title: 'Major League Fishing', - sub_title: 'Challenge Cup Sudden Death Round 2', - description: - 'Nine anglers race to a target weight on Lake Wylie in the Lucas Oil Challenge Cup, presented by B&W Trailer Hitches, Rock Hill, South Carolina. Only four will move on to the Championship Round.', - icon: 'https://content.osgnetworks.tv/shows/major-league-fishing-thumbnail.jpg' - }) - - expect(results[41]).toMatchObject({ - start: '2023-01-25T04:30:00.000Z', - stop: '2023-01-25T05:00:00.000Z', - title: 'Fishing 411', - sub_title: 'Flint Wilderness Walleye', - description: - 'Mark Romanack and Bryan Darland fish walleye on Klotz Lake in the famed Flint Wilderness of Ontario', - icon: 'https://content.osgnetworks.tv/shows/fishin-411-thumbnail.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=worldfishingnetwork.com + +const { parser, url } = require('./worldfishingnetwork.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-24', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.worldfishingnetwork.com/schedule/77420?day=Tue') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-24T05:00:00.000Z', + stop: '2023-01-24T07:00:00.000Z', + title: 'Major League Fishing', + sub_title: 'Challenge Cup Sudden Death Round 2', + description: + 'Nine anglers race to a target weight on Lake Wylie in the Lucas Oil Challenge Cup, presented by B&W Trailer Hitches, Rock Hill, South Carolina. Only four will move on to the Championship Round.', + icon: 'https://content.osgnetworks.tv/shows/major-league-fishing-thumbnail.jpg' + }) + + expect(results[41]).toMatchObject({ + start: '2023-01-25T04:30:00.000Z', + stop: '2023-01-25T05:00:00.000Z', + title: 'Fishing 411', + sub_title: 'Flint Wilderness Walleye', + description: + 'Mark Romanack and Bryan Darland fish walleye on Klotz Lake in the famed Flint Wilderness of Ontario', + icon: 'https://content.osgnetworks.tv/shows/fishin-411-thumbnail.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/xumo.tv/xumo.tv.config.js b/sites/xumo.tv/xumo.tv.config.js index 345be7203..970e16520 100644 --- a/sites/xumo.tv/xumo.tv.config.js +++ b/sites/xumo.tv/xumo.tv.config.js @@ -1,133 +1,133 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' - -const client = axios.create({ - baseURL: API_ENDPOINT, - responseType: 'arraybuffer' -}) - -module.exports = { - site: 'xumo.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - const [offset] = channel.site_id.split('#') - - return `${API_ENDPOINT}/epg/10006/${date.format( - 'YYYYMMDD' - )}/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const d = date.format('YYYYMMDD') - const [offset] = channel.site_id.split('#') - const promises = [ - client.get( - `/epg/10006/${d}/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ), - client.get( - `/epg/10006/${d}/2.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ), - client.get( - `/epg/10006/${d}/3.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ) - ] - const results = await Promise.allSettled(promises) - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data, channel)) - } - }) - - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.episodeTitle, - description: parseDescription(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get( - 'https://valencia-app-mds.xumo.com/v2/channels/list/10006.json?sort=hybrid&geoId=unknown' - ) - .then(r => r.data.channel.item) - .catch(console.log) - - const promises = [ - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=0`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=50`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=100`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=150`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=200`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=250`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=300`) - ] - - const output = [] - const results = await Promise.allSettled(promises) - results.forEach((r, i) => { - if (r.status !== 'fulfilled') return - - r.value.data.channels.forEach(item => { - const info = channels.find(c => c.guid.value == item.channelId) - - if (!info) { - console.log(item.channelId) - } - - output.push({ - site_id: `${i * 50}#${item.channelId}`, - name: info.title - }) - }) - }) - - return output - } -} - -function parseDescription(item) { - if (!item.descriptions) return null - - return item.descriptions.medium || item.descriptions.small || item.descriptions.tiny -} - -function parseStart(item) { - return dayjs(item.start) -} - -function parseStop(item) { - return dayjs(item.end) -} - -function parseItems(content, channel) { - if (!content) return [] - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(c => c.channelId == channelId) - if (!channelData || !Array.isArray(channelData.schedule)) return [] - - return channelData.schedule - .map(item => { - const details = data.assets[item.assetId] - if (!details) return null - - return { ...item, ...details } - }) - .filter(Boolean) -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' + +const client = axios.create({ + baseURL: API_ENDPOINT, + responseType: 'arraybuffer' +}) + +module.exports = { + site: 'xumo.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + const [offset] = channel.site_id.split('#') + + return `${API_ENDPOINT}/epg/10006/${date.format( + 'YYYYMMDD' + )}/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const d = date.format('YYYYMMDD') + const [offset] = channel.site_id.split('#') + const promises = [ + client.get( + `/epg/10006/${d}/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ), + client.get( + `/epg/10006/${d}/2.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ), + client.get( + `/epg/10006/${d}/3.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ) + ] + const results = await Promise.allSettled(promises) + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data, channel)) + } + }) + + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.episodeTitle, + description: parseDescription(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get( + 'https://valencia-app-mds.xumo.com/v2/channels/list/10006.json?sort=hybrid&geoId=unknown' + ) + .then(r => r.data.channel.item) + .catch(console.log) + + const promises = [ + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=0`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=50`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=100`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=150`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=200`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=250`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=300`) + ] + + const output = [] + const results = await Promise.allSettled(promises) + results.forEach((r, i) => { + if (r.status !== 'fulfilled') return + + r.value.data.channels.forEach(item => { + const info = channels.find(c => c.guid.value == item.channelId) + + if (!info) { + console.log(item.channelId) + } + + output.push({ + site_id: `${i * 50}#${item.channelId}`, + name: info.title + }) + }) + }) + + return output + } +} + +function parseDescription(item) { + if (!item.descriptions) return null + + return item.descriptions.medium || item.descriptions.small || item.descriptions.tiny +} + +function parseStart(item) { + return dayjs(item.start) +} + +function parseStop(item) { + return dayjs(item.end) +} + +function parseItems(content, channel) { + if (!content) return [] + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(c => c.channelId == channelId) + if (!channelData || !Array.isArray(channelData.schedule)) return [] + + return channelData.schedule + .map(item => { + const details = data.assets[item.assetId] + if (!details) return null + + return { ...item, ...details } + }) + .filter(Boolean) +} diff --git a/sites/xumo.tv/xumo.tv.test.js b/sites/xumo.tv/xumo.tv.test.js index a0556e8af..5af00a130 100644 --- a/sites/xumo.tv/xumo.tv.test.js +++ b/sites/xumo.tv/xumo.tv.test.js @@ -1,77 +1,77 @@ -// npm run channels:parse -- --config=./sites/xumo.tv/xumo.tv.config.js --output=./sites/xumo.tv/xumo.tv.channels.xml -// npm run grab -- --site=xumo.tv - -const { parser, url } = require('./xumo.tv.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios', () => { - return { - create: jest.fn().mockReturnValue({ - get: jest.fn() - }) - } -}) - -const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' - -const date = dayjs.utc('2022-11-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0#99991247', - xmltv_id: 'NBCNewsNow.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - `${API_ENDPOINT}/epg/10006/20221106/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.json')) - - axios.create().get.mockImplementation(url => { - if ( - url === - `${API_ENDPOINT}/epg/10006/20221106/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` - ) { - return Promise.resolve({ - data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-05T23:00:00.000Z', - stop: '2022-11-06T01:00:00.000Z', - title: 'Dateline', - sub_title: 'The Disappearance of Laci Peterson', - description: - "After following Laci Peterson's case for more than 15 years, the show delivers a comprehensive report with rarely seen interrogation video, new insight from prosecutors, and surprising details from Amber Frey, who helped uncover the truth." - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - content: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))), - channel, - date - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/xumo.tv/xumo.tv.config.js --output=./sites/xumo.tv/xumo.tv.channels.xml +// npm run grab -- --site=xumo.tv + +const { parser, url } = require('./xumo.tv.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios', () => { + return { + create: jest.fn().mockReturnValue({ + get: jest.fn() + }) + } +}) + +const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' + +const date = dayjs.utc('2022-11-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0#99991247', + xmltv_id: 'NBCNewsNow.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + `${API_ENDPOINT}/epg/10006/20221106/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.json')) + + axios.create().get.mockImplementation(url => { + if ( + url === + `${API_ENDPOINT}/epg/10006/20221106/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` + ) { + return Promise.resolve({ + data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-05T23:00:00.000Z', + stop: '2022-11-06T01:00:00.000Z', + title: 'Dateline', + sub_title: 'The Disappearance of Laci Peterson', + description: + "After following Laci Peterson's case for more than 15 years, the show delivers a comprehensive report with rarely seen interrogation video, new insight from prosecutors, and surprising details from Amber Frey, who helped uncover the truth." + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + content: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))), + channel, + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/zap.co.ao/zap.co.ao.config.js b/sites/zap.co.ao/zap.co.ao.config.js index 165900a0d..267ac8595 100644 --- a/sites/zap.co.ao/zap.co.ao.config.js +++ b/sites/zap.co.ao/zap.co.ao.config.js @@ -1,48 +1,48 @@ -const { DateTime } = require('luxon') -const axios = require('axios') - -module.exports = { - site: 'zap.co.ao', - days: 2, - url: function ({ date, channel }) { - return `https://zapon.zapsi.net/ao/m/api/epg/events?date=${date.format('YYYYMMDD')}&channel=${ - channel.site_id - }` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.programName, - description: item.programDescription, - category: item.categoryName, - start: DateTime.fromSeconds(item.utcBeginDate).toUTC(), - stop: DateTime.fromSeconds(item.utcEndDate).toUTC() - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://zapon.zapsi.net/ao/m/api/epg/channels') - .then(r => r.data.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'pt', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.data || [] -} +const { DateTime } = require('luxon') +const axios = require('axios') + +module.exports = { + site: 'zap.co.ao', + days: 2, + url: function ({ date, channel }) { + return `https://zapon.zapsi.net/ao/m/api/epg/events?date=${date.format('YYYYMMDD')}&channel=${ + channel.site_id + }` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.programName, + description: item.programDescription, + category: item.categoryName, + start: DateTime.fromSeconds(item.utcBeginDate).toUTC(), + stop: DateTime.fromSeconds(item.utcEndDate).toUTC() + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://zapon.zapsi.net/ao/m/api/epg/channels') + .then(r => r.data.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'pt', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.data || [] +} diff --git a/sites/zap.co.ao/zap.co.ao.test.js b/sites/zap.co.ao/zap.co.ao.test.js index 7331a16dd..5481dd6d5 100644 --- a/sites/zap.co.ao/zap.co.ao.test.js +++ b/sites/zap.co.ao/zap.co.ao.test.js @@ -1,48 +1,48 @@ -// npm run channels:parse -- --config=./sites/zap.co.ao/zap.co.ao.config.js --output=./sites/zap.co.ao/zap.co.ao.channels.xml -// npm run grab -- --site=zap.co.ao - -const { parser, url } = require('./zap.co.ao.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-05-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2275', - xmltv_id: 'TPA1.ao' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://zapon.zapsi.net/ao/m/api/epg/events?date=20230528&channel=2275' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-05-27T23:00:00.000Z', - stop: '2023-05-28T00:00:00.000Z', - title: 'Jornal da Meia-Noite', - description: - 'Um jornal diferente do Telejornal, por conter análise, comentários e coluna com jornalistas experientes sobre factos do dia a dia.', - category: 'Noticiário' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '[]' - }) - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/zap.co.ao/zap.co.ao.config.js --output=./sites/zap.co.ao/zap.co.ao.channels.xml +// npm run grab -- --site=zap.co.ao + +const { parser, url } = require('./zap.co.ao.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-05-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2275', + xmltv_id: 'TPA1.ao' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://zapon.zapsi.net/ao/m/api/epg/events?date=20230528&channel=2275' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-05-27T23:00:00.000Z', + stop: '2023-05-28T00:00:00.000Z', + title: 'Jornal da Meia-Noite', + description: + 'Um jornal diferente do Telejornal, por conter análise, comentários e coluna com jornalistas experientes sobre factos do dia a dia.', + category: 'Noticiário' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '[]' + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/ziggogo.tv/ziggogo.tv.config.js b/sites/ziggogo.tv/ziggogo.tv.config.js index 3fb147c65..42e171beb 100644 --- a/sites/ziggogo.tv/ziggogo.tv.config.js +++ b/sites/ziggogo.tv/ziggogo.tv.config.js @@ -1,133 +1,133 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite' - -module.exports = { - site: 'ziggogo.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - return `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const promises = [ - axios.get( - `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date - .add(6, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date - .add(12, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date - .add(18, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ) - ] - - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - const parsed = parseItems(r.value.data, channel) - - items = items.concat(parsed) - } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item, channel) - programs.push({ - title: item.title, - description: detail.longDescription, - category: detail.genres, - actors: detail.actors, - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get( - 'https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/channels?cityId=65535&language=en&productClass=Orion-DASH' - ) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'be', - site_id: item.id, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item, channel) { - if (!item.id) return {} - const url = `https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.entries)) return [] - const channelData = data.entries.find(e => e.channelId === channel.site_id) - if (!channelData) return [] - - return Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(detail) { - if (!detail.seasonNumber) return null - if (String(detail.seasonNumber).length > 2) return null - return detail.seasonNumber -} - -function parseEpisode(detail) { - if (!detail.episodeNumber) return null - if (String(detail.episodeNumber).length > 3) return null - return detail.episodeNumber -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite' + +module.exports = { + site: 'ziggogo.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + return `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const promises = [ + axios.get( + `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date + .add(6, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date + .add(12, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date + .add(18, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ) + ] + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item, channel) + programs.push({ + title: item.title, + description: detail.longDescription, + category: detail.genres, + actors: detail.actors, + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get( + 'https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/channels?cityId=65535&language=en&productClass=Orion-DASH' + ) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'be', + site_id: item.id, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item, channel) { + if (!item.id) return {} + const url = `https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.entries)) return [] + const channelData = data.entries.find(e => e.channelId === channel.site_id) + if (!channelData) return [] + + return Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(detail) { + if (!detail.seasonNumber) return null + if (String(detail.seasonNumber).length > 2) return null + return detail.seasonNumber +} + +function parseEpisode(detail) { + if (!detail.episodeNumber) return null + if (String(detail.episodeNumber).length > 3) return null + return detail.episodeNumber +} diff --git a/sites/ziggogo.tv/ziggogo.tv.test.js b/sites/ziggogo.tv/ziggogo.tv.test.js index 502f46ec3..0ec9c570b 100644 --- a/sites/ziggogo.tv/ziggogo.tv.test.js +++ b/sites/ziggogo.tv/ziggogo.tv.test.js @@ -1,98 +1,98 @@ -// npm run channels:parse -- --config=./sites/ziggogo.tv/ziggogo.tv.config.js --output=./sites/ziggogo.tv/ziggogo.tv.channels.xml -// npm run grab -- --site=ziggogo.tv - -const { parser, url } = require('./ziggogo.tv.config.js') -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') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'NL_000001_019401', - xmltv_id: 'NPO1.nl', - lang: 'nl' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028000000' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028060000' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) - }) - } else if ( - url === - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028120000' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) - }) - } else if ( - url === - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028180000' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) - }) - } else if ( - url === - 'https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F817615~~2FSH010806510000~~2F144222201,imi:ea187e3432c4a98b5ea45bcc5525c7a93c77b47b?returnLinearContent=true&language=nl' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-27T23:40:00.000Z', - stop: '2022-10-28T00:07:00.000Z', - title: 'NOS Journaal', - description: - 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor de avond en komende dagen.', - category: ['Nieuws'], - actors: [ - 'Malou Petter', - 'Mark Visser', - 'Rob Trip', - 'Jeroen Overbeek', - 'Simone Weimans', - 'Annechien Steenhuizen', - 'Jeroen Tjepkema', - 'Saïda Maggé', - 'Winfried Baijens' - ] - }) -}) - -it('can handle empty guide', async () => { - let results = await parser({ content: '', channel, date }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/ziggogo.tv/ziggogo.tv.config.js --output=./sites/ziggogo.tv/ziggogo.tv.channels.xml +// npm run grab -- --site=ziggogo.tv + +const { parser, url } = require('./ziggogo.tv.config.js') +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') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'NL_000001_019401', + xmltv_id: 'NPO1.nl', + lang: 'nl' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028000000' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028060000' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) + }) + } else if ( + url === + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028120000' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) + }) + } else if ( + url === + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028180000' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) + }) + } else if ( + url === + 'https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F817615~~2FSH010806510000~~2F144222201,imi:ea187e3432c4a98b5ea45bcc5525c7a93c77b47b?returnLinearContent=true&language=nl' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-27T23:40:00.000Z', + stop: '2022-10-28T00:07:00.000Z', + title: 'NOS Journaal', + description: + 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor de avond en komende dagen.', + category: ['Nieuws'], + actors: [ + 'Malou Petter', + 'Mark Visser', + 'Rob Trip', + 'Jeroen Overbeek', + 'Simone Weimans', + 'Annechien Steenhuizen', + 'Jeroen Tjepkema', + 'Saïda Maggé', + 'Winfried Baijens' + ] + }) +}) + +it('can handle empty guide', async () => { + let results = await parser({ content: '', channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/znbc.co.zm/znbc.co.zm.config.js b/sites/znbc.co.zm/znbc.co.zm.config.js index 555d51e0d..3d020e88f 100644 --- a/sites/znbc.co.zm/znbc.co.zm.config.js +++ b/sites/znbc.co.zm/znbc.co.zm.config.js @@ -1,65 +1,65 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const tabletojson = require('tabletojson').Tabletojson - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'znbc.co.zm', - days: 2, - url({ channel }) { - return `https://www.znbc.co.zm/${channel.site_id}/` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ title: item.title, start, stop }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Africa/Lusaka') -} - -function parseItems(content, date) { - const dayOfWeek = date.format('dddd').toUpperCase() - const $ = cheerio.load(content) - const table = $(`.elementor-tab-mobile-title:contains("${dayOfWeek}")`).next().html() - if (!table) return [] - const data = tabletojson.convert(table) - if (!Array.isArray(data) || !Array.isArray(data[0])) return [] - - return data[0] - .map(row => { - const [, time, title] = row['0'].replace(/\s\s/g, ' ').match(/^(\d{2}:\d{2}) (.*)/) || [ - null, - null, - null - ] - if (!time || !title.trim()) return null - - return { time, title: title.trim() } - }) - .filter(i => i) -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const tabletojson = require('tabletojson').Tabletojson + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'znbc.co.zm', + days: 2, + url({ channel }) { + return `https://www.znbc.co.zm/${channel.site_id}/` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ title: item.title, start, stop }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Africa/Lusaka') +} + +function parseItems(content, date) { + const dayOfWeek = date.format('dddd').toUpperCase() + const $ = cheerio.load(content) + const table = $(`.elementor-tab-mobile-title:contains("${dayOfWeek}")`).next().html() + if (!table) return [] + const data = tabletojson.convert(table) + if (!Array.isArray(data) || !Array.isArray(data[0])) return [] + + return data[0] + .map(row => { + const [, time, title] = row['0'].replace(/\s\s/g, ' ').match(/^(\d{2}:\d{2}) (.*)/) || [ + null, + null, + null + ] + if (!time || !title.trim()) return null + + return { time, title: title.trim() } + }) + .filter(i => i) +} diff --git a/sites/znbc.co.zm/znbc.co.zm.test.js b/sites/znbc.co.zm/znbc.co.zm.test.js index b8380dd45..f5d8185f8 100644 --- a/sites/znbc.co.zm/znbc.co.zm.test.js +++ b/sites/znbc.co.zm/znbc.co.zm.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=znbc.co.zm - -const { parser, url } = require('./znbc.co.zm.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tv1', - xmltv_id: 'ZNBCTV1.zm' -} -const content = - '
        ' - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.znbc.co.zm/tv1/') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T22:00:00.000Z', - stop: '2021-11-24T23:00:00.000Z', - title: 'MAIN NEWS – RPT' - }, - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-25T00:00:00.000Z', - title: 'BORN & BRED – Rebroadcast (Tuesday Edition)' - }, - { - start: '2021-11-25T00:00:00.000Z', - stop: '2021-11-25T00:30:00.000Z', - title: 'DOCUMENTARY – DW' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=znbc.co.zm + +const { parser, url } = require('./znbc.co.zm.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tv1', + xmltv_id: 'ZNBCTV1.zm' +} +const content = + '
        ' + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.znbc.co.zm/tv1/') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T22:00:00.000Z', + stop: '2021-11-24T23:00:00.000Z', + title: 'MAIN NEWS – RPT' + }, + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-25T00:00:00.000Z', + title: 'BORN & BRED – Rebroadcast (Tuesday Edition)' + }, + { + start: '2021-11-25T00:00:00.000Z', + stop: '2021-11-25T00:30:00.000Z', + title: 'DOCUMENTARY – DW' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/zuragt.mn/zuragt.mn.config.js b/sites/zuragt.mn/zuragt.mn.config.js index ed197342d..e20681b42 100644 --- a/sites/zuragt.mn/zuragt.mn.config.js +++ b/sites/zuragt.mn/zuragt.mn.config.js @@ -1,89 +1,89 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'zuragt.mn', - days: 2, - url: function ({ channel, date }) { - return `https://m.zuragt.mn/channel/${channel.site_id}/?date=${date.format('YYYY-MM-DD')}` - }, - request: { - maxRedirects: 0, - validateStatus: function (status) { - return status >= 200 && status < 303 - } - }, - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - } - - return programs - }, - async channels() { - let html = await axios - .get('https://www.zuragt.mn/') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - - const items = $('.tv-box > ul > li').toArray() - return items - .map(item => { - const name = $(item).text().trim() - const link = $(item).find('a').attr('href') - - if (!link) return null - - const [, site_id] = link.match(/\/channel\/(.*)\//) || [null, null] - - return { - lang: 'mn', - site_id, - name - } - }) - .filter(Boolean) - } -} - -function parseTitle($item) { - return $item('.program').text().trim() -} - -function parseStart($item, date) { - const time = $item('.time') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Ulaanbaatar') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('body > div > div > div > ul > li').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'zuragt.mn', + days: 2, + url: function ({ channel, date }) { + return `https://m.zuragt.mn/channel/${channel.site_id}/?date=${date.format('YYYY-MM-DD')}` + }, + request: { + maxRedirects: 0, + validateStatus: function (status) { + return status >= 200 && status < 303 + } + }, + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + } + + return programs + }, + async channels() { + let html = await axios + .get('https://www.zuragt.mn/') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + + const items = $('.tv-box > ul > li').toArray() + return items + .map(item => { + const name = $(item).text().trim() + const link = $(item).find('a').attr('href') + + if (!link) return null + + const [, site_id] = link.match(/\/channel\/(.*)\//) || [null, null] + + return { + lang: 'mn', + site_id, + name + } + }) + .filter(Boolean) + } +} + +function parseTitle($item) { + return $item('.program').text().trim() +} + +function parseStart($item, date) { + const time = $item('.time') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Ulaanbaatar') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('body > div > div > div > ul > li').toArray() +} diff --git a/sites/zuragt.mn/zuragt.mn.test.js b/sites/zuragt.mn/zuragt.mn.test.js index 9c9be9923..1d8d75a2f 100644 --- a/sites/zuragt.mn/zuragt.mn.test.js +++ b/sites/zuragt.mn/zuragt.mn.test.js @@ -1,49 +1,49 @@ -// npm run channels:parse -- --config=./sites/zuragt.mn/zuragt.mn.config.js --output=./sites/zuragt.mn/zuragt.mn.channels.xml -// npm run grab -- --site=zuragt.mn - -const { parser, url, request } = require('./zuragt.mn.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'mnb', - xmltv_id: 'MNB.mn' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://m.zuragt.mn/channel/mnb/?date=2023-01-15') -}) - -it('can generate valid request object', () => { - expect(request.maxRedirects).toBe(0) - expect(request.validateStatus(302)).toBe(true) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-14T23:00:00.000Z', - stop: '2023-01-15T00:00:00.000Z', - title: '“Цагийн хүрд” мэдээллийн хөтөлбөр' - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ content: '' }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/zuragt.mn/zuragt.mn.config.js --output=./sites/zuragt.mn/zuragt.mn.channels.xml +// npm run grab -- --site=zuragt.mn + +const { parser, url, request } = require('./zuragt.mn.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'mnb', + xmltv_id: 'MNB.mn' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://m.zuragt.mn/channel/mnb/?date=2023-01-15') +}) + +it('can generate valid request object', () => { + expect(request.maxRedirects).toBe(0) + expect(request.validateStatus(302)).toBe(true) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-14T23:00:00.000Z', + stop: '2023-01-15T00:00:00.000Z', + title: '“Цагийн хүрд” мэдээллийн хөтөлбөр' + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/tests/__data__/input/channels-parse/channels-parse.config.js b/tests/__data__/input/channels-parse/channels-parse.config.js index 4688835f2..6829ec218 100644 --- a/tests/__data__/input/channels-parse/channels-parse.config.js +++ b/tests/__data__/input/channels-parse/channels-parse.config.js @@ -1,21 +1,21 @@ -module.exports = { - site: 'parse-channels.com', - url: 'https://parse-channels.com', - parser() { - return [] - }, - channels() { - return [ - { - lang: 'en', - site_id: 140, - name: 'CNN International' - }, - { - lang: 'en', - site_id: 240, - name: 'BBC World News' - } - ] - } -} +module.exports = { + site: 'parse-channels.com', + url: 'https://parse-channels.com', + parser() { + return [] + }, + channels() { + return [ + { + lang: 'en', + site_id: 140, + name: 'CNN International' + }, + { + lang: 'en', + site_id: 240, + name: 'BBC World News' + } + ] + } +} diff --git a/tests/__data__/input/epg-grab/sites/example.com/example.com.config.js b/tests/__data__/input/epg-grab/sites/example.com/example.com.config.js index 51b43a62a..7f65c30cb 100644 --- a/tests/__data__/input/epg-grab/sites/example.com/example.com.config.js +++ b/tests/__data__/input/epg-grab/sites/example.com/example.com.config.js @@ -1,28 +1,28 @@ -module.exports = { - site: 'example.com', - days: 2, - request: { - timeout: 1000 - }, - url: 'https://example.com', - parser({ channel, date }) { - if (channel.xmltv_id === 'Channel2.us') return [] - else if (channel.xmltv_id === 'Channel1.us' && channel.lang === 'fr') { - return [ - { - title: 'Programme1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } - - return [ - { - title: 'Program1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } -} +module.exports = { + site: 'example.com', + days: 2, + request: { + timeout: 1000 + }, + url: 'https://example.com', + parser({ channel, date }) { + if (channel.xmltv_id === 'Channel2.us') return [] + else if (channel.xmltv_id === 'Channel1.us' && channel.lang === 'fr') { + return [ + { + title: 'Programme1 (example.com)', + start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } + + return [ + { + title: 'Program1 (example.com)', + start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } +} diff --git a/tests/__data__/input/epg-grab/sites/example2.com/example2.com.config.js b/tests/__data__/input/epg-grab/sites/example2.com/example2.com.config.js index 68561cc84..8789ccdc4 100644 --- a/tests/__data__/input/epg-grab/sites/example2.com/example2.com.config.js +++ b/tests/__data__/input/epg-grab/sites/example2.com/example2.com.config.js @@ -1,23 +1,23 @@ -module.exports = { - site: 'example2.com', - url: 'https://example2.com', - parser({ channel, date }) { - if (channel.lang === 'fr') { - return [ - { - title: 'Programme1 (example2.com)', - start: `${date.format('YYYY-MM-DD')}T04:40:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } - - return [ - { - title: 'Program1 (example2.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } -} +module.exports = { + site: 'example2.com', + url: 'https://example2.com', + parser({ channel, date }) { + if (channel.lang === 'fr') { + return [ + { + title: 'Programme1 (example2.com)', + start: `${date.format('YYYY-MM-DD')}T04:40:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } + + return [ + { + title: 'Program1 (example2.com)', + start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } +} diff --git a/tests/commands/channels/editor.test.ts b/tests/commands/channels/editor.test.ts index 36c2f16cf..6abc50a7e 100644 --- a/tests/commands/channels/editor.test.ts +++ b/tests/commands/channels/editor.test.ts @@ -1,51 +1,51 @@ -import fs from 'fs-extra' -import { execSync } from 'child_process' -import os from 'os' -import path from 'path' - -type ExecError = { - status: number - stdout: string -} - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') - fs.copySync( - 'tests/__data__/input/channels-editor/channels-editor.channels.xml', - 'tests/__data__/output/channels.xml' - ) -}) - -describe('channels:editor', () => { - it('shows list of options for a channel', () => { - let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' - if (os.platform() === 'win32') { - ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' - } - - try { - const cmd = `${ENV_VAR} npm run channels:editor -- tests/__data__/output/channels.xml` - execSync(cmd, { encoding: 'utf8' }) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain('CNN International | CNNInternational.us [new]') - expect((error as ExecError).stdout).toContain( - 'CNN International Europe | CNNInternationalEurope.us [api]' - ) - expect((error as ExecError).stdout).toContain('Overwrite') - expect((error as ExecError).stdout).toContain('Skip') - expect((error as ExecError).stdout).toContain( - "File 'tests/__data__/output/channels.xml' successfully saved" - ) - expect(content('tests/__data__/output/channels.xml')).toEqual( - content('tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml') - ) - } - }) -}) - -function content(filepath: string) { - return fs.readFileSync(path.resolve(filepath), { - encoding: 'utf8' - }) -} +import fs from 'fs-extra' +import { execSync } from 'child_process' +import os from 'os' +import path from 'path' + +type ExecError = { + status: number + stdout: string +} + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.copySync( + 'tests/__data__/input/channels-editor/channels-editor.channels.xml', + 'tests/__data__/output/channels.xml' + ) +}) + +describe('channels:editor', () => { + it('shows list of options for a channel', () => { + let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' + if (os.platform() === 'win32') { + ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' + } + + try { + const cmd = `${ENV_VAR} npm run channels:editor -- tests/__data__/output/channels.xml` + execSync(cmd, { encoding: 'utf8' }) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain('CNN International | CNNInternational.us [new]') + expect((error as ExecError).stdout).toContain( + 'CNN International Europe | CNNInternationalEurope.us [api]' + ) + expect((error as ExecError).stdout).toContain('Overwrite') + expect((error as ExecError).stdout).toContain('Skip') + expect((error as ExecError).stdout).toContain( + "File 'tests/__data__/output/channels.xml' successfully saved" + ) + expect(content('tests/__data__/output/channels.xml')).toEqual( + content('tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml') + ) + } + }) +}) + +function content(filepath: string) { + return fs.readFileSync(path.resolve(filepath), { + encoding: 'utf8' + }) +} diff --git a/tests/commands/channels/lint.test.ts b/tests/commands/channels/lint.test.ts index 043c6e1b6..2a772fe5a 100644 --- a/tests/commands/channels/lint.test.ts +++ b/tests/commands/channels/lint.test.ts @@ -1,22 +1,22 @@ -import { execSync } from 'child_process' - -type ExecError = { - status: number - stdout: string -} - -describe('channels:lint', () => { - it('will show a message if the file contains a syntax error', () => { - try { - const cmd = - 'npm run channels:lint -- --channels=tests/__data__/input/channels-lint/channels-lint.channels.xml' - execSync(cmd, { encoding: 'utf8' }) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain( - "channels-lint.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n" - ) - } - }) -}) +import { execSync } from 'child_process' + +type ExecError = { + status: number + stdout: string +} + +describe('channels:lint', () => { + it('will show a message if the file contains a syntax error', () => { + try { + const cmd = + 'npm run channels:lint -- --channels=tests/__data__/input/channels-lint/channels-lint.channels.xml' + execSync(cmd, { encoding: 'utf8' }) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain( + "channels-lint.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n" + ) + } + }) +}) diff --git a/tests/commands/channels/parse.test.ts b/tests/commands/channels/parse.test.ts index 83d91d7ae..b50dda476 100644 --- a/tests/commands/channels/parse.test.ts +++ b/tests/commands/channels/parse.test.ts @@ -1,39 +1,39 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' -import path from 'path' - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') - fs.copySync( - 'tests/__data__/input/channels-parse/channels-parse.channels.xml', - 'tests/__data__/output/channels.xml' - ) -}) - -describe('channels:parse', () => { - it('can parse channels', () => { - const cmd = - 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml' - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/channels.xml')).toEqual( - content('tests/__data__/expected/sites/channels-parse/channels-parse.channels.xml') - ) - }) - - it('can parse channels with clean flag', () => { - const cmd = - 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml --clean' - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/channels.xml')).toEqual( - content('tests/__data__/expected/sites/channels-parse/channels-parse-clean.channels.xml') - ) - }) -}) - -function content(filepath: string) { - return fs.readFileSync(path.resolve(filepath), { - encoding: 'utf8' - }) -} +import { execSync } from 'child_process' +import fs from 'fs-extra' +import path from 'path' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.copySync( + 'tests/__data__/input/channels-parse/channels-parse.channels.xml', + 'tests/__data__/output/channels.xml' + ) +}) + +describe('channels:parse', () => { + it('can parse channels', () => { + const cmd = + 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml' + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/channels.xml')).toEqual( + content('tests/__data__/expected/sites/channels-parse/channels-parse.channels.xml') + ) + }) + + it('can parse channels with clean flag', () => { + const cmd = + 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml --clean' + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/channels.xml')).toEqual( + content('tests/__data__/expected/sites/channels-parse/channels-parse-clean.channels.xml') + ) + }) +}) + +function content(filepath: string) { + return fs.readFileSync(path.resolve(filepath), { + encoding: 'utf8' + }) +} diff --git a/tests/commands/channels/validate.test.ts b/tests/commands/channels/validate.test.ts index 3a305051c..0177a6e57 100644 --- a/tests/commands/channels/validate.test.ts +++ b/tests/commands/channels/validate.test.ts @@ -1,48 +1,48 @@ -import { execSync } from 'child_process' -import os from 'os' - -type ExecError = { - status: number - stdout: string -} - -let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' -if (os.platform() === 'win32') { - ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' -} - -describe('channels:validate', () => { - it('will show a message if the file contains a duplicate', () => { - try { - const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/duplicate.channels.xml` - execSync(cmd, { encoding: 'utf8' }) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain(`duplicate.channels.xml -┌─────────┬─────────────┬──────┬────────────────┬─────────┬─────────┐ -│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ -├─────────┼─────────────┼──────┼────────────────┼─────────┼─────────┤ -│ 0 │ 'duplicate' │ 'en' │ 'BravoEast.us' │ '140' │ 'Bravo' │ -└─────────┴─────────────┴──────┴────────────────┴─────────┴─────────┘ -\n1 error(s) in 1 file(s)\n`) - } - }) - - it('will show a message if the file contains a channel with wrong xmltv_id', () => { - try { - const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/wrong_xmltv_id.channels.xml` - execSync(cmd, { encoding: 'utf8' }) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain(`wrong_xmltv_id.channels.xml -┌─────────┬──────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ -│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ -├─────────┼──────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ -│ 0 │ 'wrong_xmltv_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ -└─────────┴──────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ -\n1 error(s) in 1 file(s)\n`) - } - }) -}) +import { execSync } from 'child_process' +import os from 'os' + +type ExecError = { + status: number + stdout: string +} + +let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' +if (os.platform() === 'win32') { + ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' +} + +describe('channels:validate', () => { + it('will show a message if the file contains a duplicate', () => { + try { + const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/duplicate.channels.xml` + execSync(cmd, { encoding: 'utf8' }) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain(`duplicate.channels.xml +┌─────────┬─────────────┬──────┬────────────────┬─────────┬─────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼─────────────┼──────┼────────────────┼─────────┼─────────┤ +│ 0 │ 'duplicate' │ 'en' │ 'BravoEast.us' │ '140' │ 'Bravo' │ +└─────────┴─────────────┴──────┴────────────────┴─────────┴─────────┘ +\n1 error(s) in 1 file(s)\n`) + } + }) + + it('will show a message if the file contains a channel with wrong xmltv_id', () => { + try { + const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/wrong_xmltv_id.channels.xml` + execSync(cmd, { encoding: 'utf8' }) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain(`wrong_xmltv_id.channels.xml +┌─────────┬──────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼──────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ +│ 0 │ 'wrong_xmltv_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ +└─────────┴──────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ +\n1 error(s) in 1 file(s)\n`) + } + }) +}) diff --git a/tests/commands/epg/grab.test.ts b/tests/commands/epg/grab.test.ts index 5d2b3b630..7c4ec98d9 100644 --- a/tests/commands/epg/grab.test.ts +++ b/tests/commands/epg/grab.test.ts @@ -1,94 +1,94 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' -import path from 'path' -import os from 'os' -import { Zip } from '@freearhey/core' - -let ENV_VAR = - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data' -if (os.platform() === 'win32') { - ENV_VAR = - 'SET "SITES_DIR=tests/__data__/input/epg-grab/sites" && SET "CURR_DATE=2022-10-20" && SET "DATA_DIR=tests/__data__/input/temp/data" &&' -} - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') -}) - -describe('epg:grab', () => { - it('can grab epg by site name', () => { - const cmd = `${ENV_VAR} npm run grab -- --site=example.com --output=tests/__data__/output/guide.xml` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/guide2.xml') - ) - }) - - it('can grab epg with multiple channels.xml files', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/guide.xml') - ) - }) - - it('can grab epg with gzip option enabled', async () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --gzip` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/guide.xml') - ) - - const zip = new Zip() - const expected = await zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) - const result = await zip.decompress(fs.readFileSync('tests/__data__/expected/guide.xml.gz')) - expect(expected).toEqual(result) - }) - - it('can grab epg with wildcard as output', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( - content('tests/__data__/expected/guides/en/example.com.xml') - ) - - expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( - content('tests/__data__/expected/guides/fr/example.com.xml') - ) - }) - - it('can grab epg then language filter enabled', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( - content('tests/__data__/expected/guides/fr/example.com.xml') - ) - }) - - it('can grab epg using custom channels list', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/guide.xml') - ) - }) - - it('it will raise an error if the timeout is exceeded', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=0` - const stdout = execSync(cmd, { encoding: 'utf8' }) - - expect(stdout).toContain('ERR: Connection timeout') - }) -}) - -function content(filepath: string) { - return fs.readFileSync(path.resolve(filepath), { - encoding: 'utf8' - }) -} +import { execSync } from 'child_process' +import fs from 'fs-extra' +import path from 'path' +import os from 'os' +import { Zip } from '@freearhey/core' + +let ENV_VAR = + 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "SITES_DIR=tests/__data__/input/epg-grab/sites" && SET "CURR_DATE=2022-10-20" && SET "DATA_DIR=tests/__data__/input/temp/data" &&' +} + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') +}) + +describe('epg:grab', () => { + it('can grab epg by site name', () => { + const cmd = `${ENV_VAR} npm run grab -- --site=example.com --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/guide2.xml') + ) + }) + + it('can grab epg with multiple channels.xml files', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/guide.xml') + ) + }) + + it('can grab epg with gzip option enabled', async () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --gzip` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/guide.xml') + ) + + const zip = new Zip() + const expected = await zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) + const result = await zip.decompress(fs.readFileSync('tests/__data__/expected/guide.xml.gz')) + expect(expected).toEqual(result) + }) + + it('can grab epg with wildcard as output', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( + content('tests/__data__/expected/guides/en/example.com.xml') + ) + + expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( + content('tests/__data__/expected/guides/fr/example.com.xml') + ) + }) + + it('can grab epg then language filter enabled', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( + content('tests/__data__/expected/guides/fr/example.com.xml') + ) + }) + + it('can grab epg using custom channels list', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/guide.xml') + ) + }) + + it('it will raise an error if the timeout is exceeded', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=0` + const stdout = execSync(cmd, { encoding: 'utf8' }) + + expect(stdout).toContain('ERR: Connection timeout') + }) +}) + +function content(filepath: string) { + return fs.readFileSync(path.resolve(filepath), { + encoding: 'utf8' + }) +} From a32f80a0ffd71ad89aa5779347b15a969f180c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= Date: Mon, 16 Oct 2023 15:30:56 +0200 Subject: [PATCH 21/22] Update mediaset.it.config.js --- sites/mediaset.it/mediaset.it.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sites/mediaset.it/mediaset.it.config.js b/sites/mediaset.it/mediaset.it.config.js index b6bcbed86..e7c3837e0 100644 --- a/sites/mediaset.it/mediaset.it.config.js +++ b/sites/mediaset.it/mediaset.it.config.js @@ -1,6 +1,7 @@ const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') dayjs.extend(utc) dayjs.extend(customParseFormat) @@ -51,4 +52,4 @@ module.exports = { function parseStart(item, date) { return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.startTime}`, 'YYYY-MM-DD HH:mm', 'Europe/Rome') -} \ No newline at end of file +} From d3649ee1a8f3219f007dd0bfd6168fc0c5280289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= Date: Mon, 16 Oct 2023 15:32:25 +0200 Subject: [PATCH 22/22] Reinstate what was PR made for --- sites/mediaset.it/mediaset.it.config.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/sites/mediaset.it/mediaset.it.config.js b/sites/mediaset.it/mediaset.it.config.js index e7c3837e0..40fe6935a 100644 --- a/sites/mediaset.it/mediaset.it.config.js +++ b/sites/mediaset.it/mediaset.it.config.js @@ -21,20 +21,10 @@ module.exports = { if (!data.events) return programs data.events.forEach(item => { - if (item.title && item.startTime && item.endTime) { - const start = dayjs - .utc(item.startTime, 'HH:mm') - .set('D', date.get('D')) - .set('M', date.get('M')) - .set('y', date.get('y')) - .toString() - - const stop = dayjs - .utc(item.endTime, 'HH:mm') - .set('D', date.get('D')) - .set('M', date.get('M')) - .set('y', date.get('y')) - .toString() + if (item.title && item.startTime && item.duration) { + const start = parseStart(item, date) + const duration = parseInt(item.duration) + const stop = start.add(duration, 'm') programs.push({ title: item.displayTitle || item.title,