Fix #31
This commit is contained in:
parent
db80b929ca
commit
d57888678d
@ -96,7 +96,8 @@ and set your `User`, `Group`, `ExecStart` and `WorkingDirectory` accordingly.
|
|||||||
Copy `.env.EXAMPLE` to `.env` and add your `YOUTUBE_API_KEY` and `ODESLI_API_KEY`.
|
Copy `.env.EXAMPLE` to `.env` and add your `YOUTUBE_API_KEY` and `ODESLI_API_KEY`.
|
||||||
To obtain one follow [YouTube's guide](https://developers.google.com/youtube/registering_an_application) to create an
|
To obtain one follow [YouTube's guide](https://developers.google.com/youtube/registering_an_application) to create an
|
||||||
_API key_.
|
_API key_.
|
||||||
If `YOUTUBE_API_KEY` is unset, no playlist will be updated.
|
If `YOUTUBE_API_KEY` is unset, no playlist will be updated. Also, _all_ YouTube links will be treated as music videos,
|
||||||
|
because the API is the only way to check if a YouTube link leads to music or something else.
|
||||||
|
|
||||||
If `ODESLI_API_KEY` is unset, your rate limit to the song.link API will be lower.
|
If `ODESLI_API_KEY` is unset, your rate limit to the song.link API will be lower.
|
||||||
|
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { HASHTAG_FILTER, MASTODON_INSTANCE, ODESLI_API_KEY } from '$env/static/private';
|
import {
|
||||||
|
HASHTAG_FILTER,
|
||||||
|
MASTODON_INSTANCE,
|
||||||
|
ODESLI_API_KEY,
|
||||||
|
YOUTUBE_API_KEY
|
||||||
|
} from '$env/static/private';
|
||||||
import { log } from '$lib/log';
|
import { log } from '$lib/log';
|
||||||
import type {
|
import type {
|
||||||
Account,
|
Account,
|
||||||
@ -28,10 +33,50 @@ import { WebSocket } from 'ws';
|
|||||||
|
|
||||||
const URL_REGEX = new RegExp(/href="(?<postUrl>[^>]+?)" target="_blank"/gm);
|
const URL_REGEX = new RegExp(/href="(?<postUrl>[^>]+?)" target="_blank"/gm);
|
||||||
const INVIDIOUS_REGEX = new RegExp(/invidious.*?watch.*?v=(?<videoId>[a-zA-Z_0-9-]+)/gm);
|
const INVIDIOUS_REGEX = new RegExp(/invidious.*?watch.*?v=(?<videoId>[a-zA-Z_0-9-]+)/gm);
|
||||||
|
const YOUTUBE_REGEX = new RegExp(
|
||||||
|
/https?:\/\/(www\.)?youtu((be.com\/.*?v=)|(\.be\/))(?<videoId>[a-zA-Z_0-9-]+)/gm
|
||||||
|
);
|
||||||
|
|
||||||
export class TimelineReader {
|
export class TimelineReader {
|
||||||
private static _instance: TimelineReader;
|
private static _instance: TimelineReader;
|
||||||
|
|
||||||
|
private static async isMusicVideo(videoId: string) {
|
||||||
|
if (!YOUTUBE_API_KEY || YOUTUBE_API_KEY === 'CHANGE_ME') {
|
||||||
|
// Assume that it *is* a music link when no YT API key is provided
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const searchParams = new URLSearchParams([
|
||||||
|
['part', 'snippet'],
|
||||||
|
['id', videoId],
|
||||||
|
['key', YOUTUBE_API_KEY]
|
||||||
|
]);
|
||||||
|
const youtubeVideoUrl = new URL(`https://www.googleapis.com/youtube/v3/videos?${searchParams}`);
|
||||||
|
const resp = await fetch(youtubeVideoUrl);
|
||||||
|
const respObj = await resp.json();
|
||||||
|
if (!respObj.items.length) {
|
||||||
|
console.warn('Could not find video with id', videoId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = respObj.items[0];
|
||||||
|
if (item.tags?.includes('music')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categorySearchParams = new URLSearchParams([
|
||||||
|
['part', 'snippet'],
|
||||||
|
['id', item.categoryId],
|
||||||
|
['key', YOUTUBE_API_KEY]
|
||||||
|
]);
|
||||||
|
const youtubeCategoryUrl = new URL(
|
||||||
|
`https://www.googleapis.com/youtube/v3/videoCategories?${categorySearchParams}`
|
||||||
|
);
|
||||||
|
const categoryTitle: string = await fetch(youtubeCategoryUrl)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((r) => r.items[0]?.title);
|
||||||
|
return categoryTitle === 'Music';
|
||||||
|
}
|
||||||
|
|
||||||
public static async getSongInfoInPost(post: Post): Promise<SongInfo[]> {
|
public static async getSongInfoInPost(post: Post): Promise<SongInfo[]> {
|
||||||
const urlMatches = post.content.matchAll(URL_REGEX);
|
const urlMatches = post.content.matchAll(URL_REGEX);
|
||||||
const songs: SongInfo[] = [];
|
const songs: SongInfo[] = [];
|
||||||
@ -85,23 +130,39 @@ export class TimelineReader {
|
|||||||
}
|
}
|
||||||
const odesliApiUrl = `https://api.song.link/v1-alpha.1/links?${odesliParams}`;
|
const odesliApiUrl = `https://api.song.link/v1-alpha.1/links?${odesliParams}`;
|
||||||
try {
|
try {
|
||||||
return fetch(odesliApiUrl).then(async (response) => {
|
const response = await fetch(odesliApiUrl);
|
||||||
if (response.status === 429) {
|
log.debug('received odesli response', response.status);
|
||||||
throw new Error('Rate limit reached', { cause: 429 });
|
if (response.status === 429) {
|
||||||
}
|
throw new Error('Rate limit reached', { cause: 429 });
|
||||||
const odesliInfo: OdesliResponse = await response.json();
|
}
|
||||||
if (!odesliInfo || !odesliInfo.entitiesByUniqueId || !odesliInfo.entityUniqueId) {
|
const odesliInfo: OdesliResponse = await response.json();
|
||||||
|
if (!odesliInfo || !odesliInfo.entitiesByUniqueId || !odesliInfo.entityUniqueId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const info = odesliInfo.entitiesByUniqueId[odesliInfo.entityUniqueId];
|
||||||
|
const platform: Platform = 'youtube';
|
||||||
|
log.debug(url, 'odesli response', info, 'YT URL', odesliInfo.linksByPlatform[platform]?.url);
|
||||||
|
if (info.platforms.includes(platform)) {
|
||||||
|
let youtubeId =
|
||||||
|
videoId ??
|
||||||
|
YOUTUBE_REGEX.exec(url.href)?.groups?.videoId ??
|
||||||
|
new URL(odesliInfo.pageUrl).pathname.split('/y/').pop();
|
||||||
|
if (youtubeId === undefined) {
|
||||||
|
log.warn('Looks like a youtube video, but could not extract a video id', url, odesliInfo);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const info = odesliInfo.entitiesByUniqueId[odesliInfo.entityUniqueId];
|
const isMusic = await TimelineReader.isMusicVideo(youtubeId);
|
||||||
const platform: Platform = 'youtube';
|
if (!isMusic) {
|
||||||
return {
|
log.debug('Probably not a music video', url, odesliInfo);
|
||||||
...info,
|
return null;
|
||||||
pageUrl: odesliInfo.pageUrl,
|
}
|
||||||
youtubeUrl: odesliInfo.linksByPlatform[platform]?.url,
|
}
|
||||||
postedUrl: url.toString()
|
return {
|
||||||
} as SongInfo;
|
...info,
|
||||||
});
|
pageUrl: odesliInfo.pageUrl,
|
||||||
|
youtubeUrl: odesliInfo.linksByPlatform[platform]?.url,
|
||||||
|
postedUrl: url.toString()
|
||||||
|
} as SongInfo;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.cause === 429) {
|
if (e instanceof Error && e.cause === 429) {
|
||||||
log.warn('song.link rate limit reached. Trying again in 10 seconds');
|
log.warn('song.link rate limit reached. Trying again in 10 seconds');
|
||||||
|
Loading…
Reference in New Issue
Block a user