diff --git a/README.md b/README.md index 328651f1..87e67468 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ this list is not final and keeps expanding over time. if support for a service y | service | video + audio | only audio | only video | metadata | rich file names | | :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: | | bilibili.com | ✅ | ✅ | ✅ | ➖ | ➖ | +| dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ | | instagram posts & stories | ✅ | ✅ | ✅ | ➖ | ➖ | | instagram reels | ✅ | ✅ | ✅ | ➖ | ➖ | | ok video | ✅ | ❌ | ❌ | ✅ | ✅ | diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 95558c8b..f1641baa 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -24,6 +24,7 @@ import pinterest from "./services/pinterest.js"; import streamable from "./services/streamable.js"; import twitch from "./services/twitch.js"; import rutube from "./services/rutube.js"; +import dailymotion from "./services/dailymotion.js"; export default async function(host, patternMatch, url, lang, obj) { assert(url instanceof URL); @@ -156,6 +157,9 @@ export default async function(host, patternMatch, url, lang, obj) { isAudioOnly: isAudioOnly }); break; + case "dailymotion": + r = await dailymotion(patternMatch); + break; default: return apiJSON(0, { t: errorUnsupported(lang) }); } diff --git a/src/modules/processing/services/dailymotion.js b/src/modules/processing/services/dailymotion.js new file mode 100644 index 00000000..1993ecad --- /dev/null +++ b/src/modules/processing/services/dailymotion.js @@ -0,0 +1,107 @@ +import HLSParser from 'hls-parser'; +import { maxVideoDuration } from '../../config.js'; + +let _token; + +function getExp(token) { + return JSON.parse( + Buffer.from(token.split('.')[1], 'base64') + ).exp * 1000; +} + +const getToken = async () => { + if (_token && getExp(_token) > new Date().getTime()) { + return _token; + } + + const req = await fetch('https://graphql.api.dailymotion.com/oauth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'User-Agent': 'dailymotion/240213162706 CFNetwork/1492.0.1 Darwin/23.3.0', + 'Authorization': 'Basic MGQyZDgyNjQwOWFmOWU3MmRiNWQ6ODcxNmJmYTVjYmEwMmUwMGJkYTVmYTg1NTliNDIwMzQ3NzIyYWMzYQ==' + }, + body: 'traffic_segment=&grant_type=client_credentials' + }).then(r => r.json()).catch(() => {}); + + if (req.access_token) { + return _token = req.access_token; + } +} + +export default async function({ id }) { + const token = await getToken(); + if (!token) return { error: 'ErrorSomethingWentWrong' }; + + const req = await fetch('https://graphql.api.dailymotion.com/', + { + method: 'POST', + headers: { + 'User-Agent': 'dailymotion/240213162706 CFNetwork/1492.0.1 Darwin/23.3.0', + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + 'X-DM-AppInfo-Version': '7.16.0_240213162706', + 'X-DM-AppInfo-Type': 'iosapp', + 'X-DM-AppInfo-Id': 'com.dailymotion.dailymotion' + }, + body: JSON.stringify({ + operationName: "Media", + query: ` + query Media($xid: String!, $password: String) { + media(xid: $xid, password: $password) { + __typename + ... on Video { + xid + hlsURL + duration + title + channel { + displayName + } + } + } + } + `, + variables: { xid: id } + }) + } + ).then(r => r.status === 200 && r.json()).catch(() => {}); + + const media = req?.data?.media; + + if (media?.__typename !== 'Video' || !media.hlsURL) { + return { error: 'ErrorEmptyDownload' } + } + + if (media.duration * 1000 > maxVideoDuration) { + return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + } + + const manifest = await fetch(media.hlsURL).then(r => r.text()).catch(() => {}); + if (!manifest) return { error: 'ErrorSomethingWentWrong' }; + + const bestQuality = HLSParser.parse(manifest).variants + .filter(v => v.codecs.includes('avc1')) + .reduce((a, b) => a.bandwidth > b.bandwidth ? a : b); + if (!bestQuality) return { error: 'ErrorEmptyDownload' } + + const fileMetadata = { + title: media.title, + artist: media.channel.displayName + } + + return { + urls: bestQuality.uri, + isM3U8: true, + filenameAttributes: { + service: 'dailymotion', + id: media.xid, + title: fileMetadata.title, + author: fileMetadata.artist, + resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`, + qualityLabel: `${bestQuality.resolution.height}p`, + extension: 'mp4' + }, + fileMetadata + } +} \ No newline at end of file diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 384ca1b6..52b8d588 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -109,6 +109,11 @@ "tld": "ru", "patterns": ["video/:id", "play/embed/:id"], "enabled": true + }, + "dailymotion": { + "alias": "dailymotion videos", + "patterns": ["video/:id"], + "enabled": true } } } diff --git a/src/modules/processing/servicesPatternTesters.js b/src/modules/processing/servicesPatternTesters.js index 30892f62..393a9c99 100644 --- a/src/modules/processing/servicesPatternTesters.js +++ b/src/modules/processing/servicesPatternTesters.js @@ -3,6 +3,8 @@ export const testers = { patternMatch.comId?.length <= 12 || patternMatch.comShortLink?.length <= 16 || patternMatch.tvId?.length <= 24, + "dailymotion": (patternMatch) => patternMatch.id?.length <= 32, + "instagram": (patternMatch) => patternMatch.postId?.length <= 12 || (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24), diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index 5e6bd15a..b272ff80 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -59,6 +59,11 @@ export function aliasURL(url) { url = new URL(`https://bilibili.com/_shortLink/${parts[1]}`) } break; + + case "dai": + if (url.hostname === 'dai.ly' && parts.length === 2) { + url = new URL(`https://dailymotion.com/video/${parts[1]}`) + } } return url diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 7dcfd74a..6a58058d 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -215,7 +215,7 @@ export function streamVideoOnly(streamInfo, res) { args.push('-an') } - if (['vimeo', 'rutube'].includes(streamInfo.service)) { + if (["vimeo", "rutube", "dailymotion"].includes(streamInfo.service)) { args.push('-bsf:a', 'aac_adtstoasc') } diff --git a/src/test/tests.json b/src/test/tests.json index 42f75f40..f31156f7 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -1197,5 +1197,30 @@ "code": 200, "status": "stream" } + }], + "dailymotion": [{ + "name": "regular video", + "url": "https://www.dailymotion.com/video/x8t1eho", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "private video", + "url": "https://www.dailymotion.com/video/k41fZWpx2TaAORA2nok", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "dai.ly shortened link", + "url": "https://dai.ly/k41fZWpx2TaAORA2nok", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } }] }