Spaces:
Paused
Paused
| import psl from "psl"; | |
| import { strict as assert } from "node:assert"; | |
| import { env } from "../config.js"; | |
| import { services } from "./service-config.js"; | |
| import { friendlyServiceName } from "./service-alias.js"; | |
| function aliasURL(url) { | |
| assert(url instanceof URL); | |
| const host = psl.parse(url.hostname); | |
| const parts = url.pathname.split('/'); | |
| switch (host.sld) { | |
| case "youtube": | |
| if (url.pathname.startsWith('/live/') || url.pathname.startsWith('/shorts/')) { | |
| url.pathname = '/watch'; | |
| // parts := ['', 'live' || 'shorts', id, ...rest] | |
| url.search = `?v=${encodeURIComponent(parts[2])}` | |
| } | |
| break; | |
| case "youtu": | |
| if (url.hostname === 'youtu.be' && parts.length >= 2) { | |
| /* youtu.be urls can be weird, e.g. https://youtu.be/<id>//asdasd// still works | |
| ** but we only care about the 1st segment of the path */ | |
| url = new URL(`https://youtube.com/watch?v=${ | |
| encodeURIComponent(parts[1]) | |
| }`) | |
| } | |
| break; | |
| case "pin": | |
| if (url.hostname === 'pin.it' && parts.length === 2) { | |
| url = new URL(`https://pinterest.com/url_shortener/${ | |
| encodeURIComponent(parts[1]) | |
| }`) | |
| } | |
| break; | |
| case "vxtwitter": | |
| case "fixvx": | |
| case "x": | |
| if (services.twitter.altDomains.includes(url.hostname)) { | |
| url.hostname = 'twitter.com' | |
| } | |
| break; | |
| case "twitch": | |
| if (url.hostname === 'clips.twitch.tv' && parts.length >= 2) { | |
| url = new URL(`https://twitch.tv/_/clip/${parts[1]}`); | |
| } | |
| break; | |
| case "bilibili": | |
| if (host.tld === 'tv') { | |
| url = new URL(`https://bilibili.com/_tv${url.pathname}`); | |
| } | |
| break; | |
| case "b23": | |
| if (url.hostname === 'b23.tv' && parts.length === 2) { | |
| 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]}`) | |
| } | |
| break; | |
| case "facebook": | |
| case "fb": | |
| if (url.searchParams.get('v')) { | |
| url = new URL(`https://web.facebook.com/user/videos/${url.searchParams.get('v')}`) | |
| } | |
| if (url.hostname === 'fb.watch') { | |
| url = new URL(`https://web.facebook.com/_shortLink/${parts[1]}`) | |
| } | |
| break; | |
| case "ddinstagram": | |
| if (services.instagram.altDomains.includes(host.domain) && [null, 'd', 'g'].includes(host.subdomain)) { | |
| url.hostname = 'instagram.com'; | |
| } | |
| break; | |
| } | |
| return url | |
| } | |
| function cleanURL(url) { | |
| assert(url instanceof URL); | |
| const host = psl.parse(url.hostname).sld; | |
| let stripQuery = true; | |
| const limitQuery = (param) => { | |
| url.search = `?${param}=` + encodeURIComponent(url.searchParams.get(param)); | |
| stripQuery = false; | |
| } | |
| switch (host) { | |
| case "pinterest": | |
| url.hostname = 'pinterest.com'; | |
| break; | |
| case "vk": | |
| if (url.pathname.includes('/clip') && url.searchParams.get('z')) { | |
| limitQuery('z') | |
| } | |
| break; | |
| case "youtube": | |
| if (url.searchParams.get('v')) { | |
| limitQuery('v') | |
| } | |
| break; | |
| case "rutube": | |
| if (url.searchParams.get('p')) { | |
| limitQuery('p') | |
| } | |
| break; | |
| } | |
| if (stripQuery) { | |
| url.search = '' | |
| } | |
| url.username = url.password = url.port = url.hash = '' | |
| if (url.pathname.endsWith('/')) | |
| url.pathname = url.pathname.slice(0, -1); | |
| return url | |
| } | |
| function getHostIfValid(url) { | |
| const host = psl.parse(url.hostname); | |
| if (host.error) return; | |
| const service = services[host.sld]; | |
| if (!service) return; | |
| if ((service.tld ?? 'com') !== host.tld) return; | |
| const anySubdomainAllowed = service.subdomains === '*'; | |
| const validSubdomain = [null, 'www', ...(service.subdomains ?? [])].includes(host.subdomain); | |
| if (!validSubdomain && !anySubdomainAllowed) return; | |
| return host.sld; | |
| } | |
| export function normalizeURL(url) { | |
| return cleanURL( | |
| aliasURL( | |
| new URL(url.replace(/^https\/\//, 'https://')) | |
| ) | |
| ); | |
| } | |
| export function extract(url) { | |
| if (!(url instanceof URL)) { | |
| url = new URL(url); | |
| } | |
| const host = getHostIfValid(url); | |
| if (!host) { | |
| return { error: "link.invalid" }; | |
| } | |
| if (!env.enabledServices.has(host)) { | |
| return { error: "service.disabled" }; | |
| } | |
| let patternMatch; | |
| for (const pattern of services[host].patterns) { | |
| patternMatch = pattern.match( | |
| url.pathname.substring(1) + url.search | |
| ); | |
| if (patternMatch) { | |
| break; | |
| } | |
| } | |
| if (!patternMatch) { | |
| return { | |
| error: "link.unsupported", | |
| context: { | |
| service: friendlyServiceName(host), | |
| } | |
| }; | |
| } | |
| return { host, patternMatch }; | |
| } | |