diff --git a/codegen.yml b/codegen.yml index 779c3f2a..eea01369 100644 --- a/codegen.yml +++ b/codegen.yml @@ -132,6 +132,7 @@ generates: VideoTitleSetPrimaryEvent: ../Video/dto.js#VideoTitleEventDTO VideoTitleUnsetPrimaryEvent: ../Video/dto.js#VideoTitleEventDTO YoutubeMadRequestedTimelineEvent: ../Timeline/TimelineEvent.dto.js#YoutubeMadRequestedTimelineEventDTO + YoutubeOriginalSource: ../FetchExternal/YoutubeOriginalSource.dto.js#YoutubeOriginalSourceDTO YoutubeRegistrationRequest: ../YoutubeRegistrationRequest/YoutubeRegistrationRequest.dto.js#YoutubeRegistrationRequestDTO YoutubeRegistrationRequestAcceptEvent: ../YoutubeRegistrationRequest/YoutubeRegistrationRequestEvent.dto.js#YoutubeRegistrationRequestAcceptEventDTO YoutubeRegistrationRequestAccepting: ../YoutubeRegistrationRequest/YoutubeRegistrationRequestAccepting.dto.js#YoutubeRegistrationRequestAcceptingDTO diff --git a/src/FetchExternal/BilibiliOriginalSource.graphql b/src/FetchExternal/BilibiliOriginalSource.graphql new file mode 100644 index 00000000..cd71942f --- /dev/null +++ b/src/FetchExternal/BilibiliOriginalSource.graphql @@ -0,0 +1,22 @@ +type BilibiliOriginalSource { + sourceId: String! + + title: String! + url: String! + + "プロキシされた画像URL" + thumbnailUrl(scale: FetchExternalSourceThumbnailScale! = LARGE): String! + @deprecated(reason: "Use `proxiedThumbnailUrl`") + + "プロキシされた画像URL" + proxiedThumbnailUrl(scale: FetchExternalSourceThumbnailScale! = LARGE): String! + + """ + bilibili側のオリジナルの画像URL + + bilibili側の制約上、別のオリジンから直接埋め込んだりすることはほとんど出来ないことに注意. + """ + originalThumbnailUrl: String! + + tags: [BilibiliOriginalSourceTag!]! +} diff --git a/src/FetchExternal/BilibiliOriginalSource.resolver.ts b/src/FetchExternal/BilibiliOriginalSource.resolver.ts index cba90c7a..16a7d1f9 100644 --- a/src/FetchExternal/BilibiliOriginalSource.resolver.ts +++ b/src/FetchExternal/BilibiliOriginalSource.resolver.ts @@ -6,7 +6,10 @@ export const mkBilibiliOriginalSourceResolver: MkResolver<"BilibiliOriginalSourc title: ({ title }) => title, sourceId: ({ sourceId }) => sourceId, tags: ({ tags }) => tags, - thumbnailUrl: ({ sourceId }, { scale }) => ImagesService.originalBilibili(sourceId, scale), + + thumbnailUrl: ({ sourceId }, { scale }) => ImagesService.proxyThis(sourceId, scale), + proxiedThumbnailUrl: ({ sourceId }, { scale }) => ImagesService.proxyThis(sourceId, scale), + originalThumbnailUrl: ({ originalThumbnailUrl }) => originalThumbnailUrl, url: ({ sourceId }) => `https://www.bilibili.com/video/${sourceId}`, }); diff --git a/src/FetchExternal/BilibiliOriginalSourceTag.graphql b/src/FetchExternal/BilibiliOriginalSourceTag.graphql new file mode 100644 index 00000000..61518f52 --- /dev/null +++ b/src/FetchExternal/BilibiliOriginalSourceTag.graphql @@ -0,0 +1,13 @@ +type BilibiliOriginalSourceTag { + name: String! + searchTags(input: BilibiliOriginalSourceTagSearchTagsInput!): BilibiliOriginalSourceTagSearchTagsPayload! +} + +input BilibiliOriginalSourceTagSearchTagsInput { + limit: Int! = 3 + skip: Int! = 0 +} + +type BilibiliOriginalSourceTagSearchTagsPayload { + items: [TagSearchItemByName!]! +} diff --git a/src/FetchExternal/SoundcloudOriginalSource.graphql b/src/FetchExternal/SoundcloudOriginalSource.graphql index b308227e..3fffb340 100644 --- a/src/FetchExternal/SoundcloudOriginalSource.graphql +++ b/src/FetchExternal/SoundcloudOriginalSource.graphql @@ -6,6 +6,10 @@ type SoundcloudOriginalSource { "プロキシしたサムネイル画像" thumbnailUrl(scale: FetchExternalSourceThumbnailScale! = LARGE): String! + @deprecated(reason: "Use `proxiedThumbnailUrl`") + + "プロキシしたサムネイル画像URL" + proxiedThumbnailUrl(scale: FetchExternalSourceThumbnailScale! = LARGE): String! """ SoundCloud側のサムネイル画像.ない場合は投稿者のユーザー画像になる. diff --git a/src/FetchExternal/SoundcloudOriginalSource.resolver.ts b/src/FetchExternal/SoundcloudOriginalSource.resolver.ts index 3ea16087..5d996984 100644 --- a/src/FetchExternal/SoundcloudOriginalSource.resolver.ts +++ b/src/FetchExternal/SoundcloudOriginalSource.resolver.ts @@ -7,6 +7,8 @@ export const mkSoundcloudOriginalSourceResolver: MkResolver<"SoundcloudOriginalS sourceId: ({ sourceId }) => sourceId, thumbnailUrl: ({ originalThumbnailUrl, userAvatarUrl }, { scale }) => ImagesService.proxyThis(originalThumbnailUrl || userAvatarUrl, scale), + proxiedThumbnailUrl: ({ originalThumbnailUrl, userAvatarUrl }, { scale }) => + ImagesService.proxyThis(originalThumbnailUrl || userAvatarUrl, scale), originalThumbnailUrl: ({ originalThumbnailUrl, userAvatarUrl }) => originalThumbnailUrl || userAvatarUrl, url: ({ url }) => url, }); diff --git a/src/FetchExternal/YoutubeOriginalSource.dto.ts b/src/FetchExternal/YoutubeOriginalSource.dto.ts new file mode 100644 index 00000000..d82103fc --- /dev/null +++ b/src/FetchExternal/YoutubeOriginalSource.dto.ts @@ -0,0 +1,33 @@ +export class YoutubeOriginalSourceDTO { + private constructor( + private readonly source: { + sourceId: string; + url: string; + originalThumbnailUrl: string; + }, + ) {} + + public static make({ + sourceId, + url, + originalThumbnailUrl, + }: { + sourceId: string; + url: string; + originalThumbnailUrl: string; + }) { + return new YoutubeOriginalSourceDTO({ sourceId, url, originalThumbnailUrl }); + } + + get sourceId() { + return this.source.sourceId; + } + + get url() { + return this.source.url; + } + + get originalThumbnailUrl() { + return this.source.originalThumbnailUrl; + } +} diff --git a/src/FetchExternal/YoutubeOriginalSource.graphql b/src/FetchExternal/YoutubeOriginalSource.graphql new file mode 100644 index 00000000..2ad7bdb5 --- /dev/null +++ b/src/FetchExternal/YoutubeOriginalSource.graphql @@ -0,0 +1,9 @@ +type YoutubeOriginalSource { + sourceId: String! + url: String! + + thumbnailUrl: String! @deprecated(reason: "Use `originalThumbnailUrl`") + originalThumbnailUrl: String! + + proxiedThumbnailUrl(scale: FetchExternalSourceThumbnailScale! = LARGE): String! +} diff --git a/src/FetchExternal/YoutubeOriginalSource.resolver.ts b/src/FetchExternal/YoutubeOriginalSource.resolver.ts new file mode 100644 index 00000000..a9fb1c27 --- /dev/null +++ b/src/FetchExternal/YoutubeOriginalSource.resolver.ts @@ -0,0 +1,13 @@ +import { MkResolver } from "../utils/MkResolver.js"; + +export const mkYoutubeOriginalSourceResolver: MkResolver<"YoutubeOriginalSource", "ImagesService"> = ({ + ImagesService, +}) => { + return { + sourceId: ({ sourceId }) => sourceId, + url: ({ sourceId }) => `https://www.nicovideo.jp/watch/${sourceId}`, + thumbnailUrl: ({ originalThumbnailUrl }) => originalThumbnailUrl, + originalThumbnailUrl: ({ originalThumbnailUrl }) => originalThumbnailUrl, + proxiedThumbnailUrl: ({ originalThumbnailUrl }, { scale }) => ImagesService.proxyThis(originalThumbnailUrl, scale), + }; +}; diff --git a/src/FetchExternal/fetchBilibili.graphql b/src/FetchExternal/fetchBilibili.graphql index b35bd6db..5f6b9574 100644 --- a/src/FetchExternal/fetchBilibili.graphql +++ b/src/FetchExternal/fetchBilibili.graphql @@ -11,37 +11,3 @@ input FetchBilibiliInput { type FetchBilibiliPayload { source: BilibiliOriginalSource } - -type BilibiliOriginalSource { - sourceId: String! - - title: String! - url: String! - - thumbnailUrl(scale: FetchExternalSourceThumbnailScale! = LARGE): String! - originalThumbnailUrl: String! - - tags: [BilibiliOriginalSourceTag!]! -} - -type BilibiliOriginalSourceTag { - name: String! - searchTags(input: BilibiliOriginalSourceTagSearchTagsInput!): BilibiliOriginalSourceTagSearchTagsPayload! -} - -input BilibiliOriginalSourceTagSearchTagsInput { - limit: Int! = 3 - skip: Int! = 0 -} - -type BilibiliOriginalSourceTagSearchTagsPayload { - items: [TagSearchItemByName!]! -} - -enum FetchExternalSourceThumbnailScale { - "960 x 700" - LARGE - - "720 x 400" - OGP -} diff --git a/src/FetchExternal/fetchNicovideo.ts b/src/FetchExternal/fetchNicovideo.ts index 43be8a0b..d9d527bd 100644 --- a/src/FetchExternal/fetchNicovideo.ts +++ b/src/FetchExternal/fetchNicovideo.ts @@ -57,6 +57,8 @@ export const fetchNicovideo = () => source: null, }; // TODO: もう少し詳細な情報を出しても良い気がする return { - source: NicovideoOriginalSourceDTO.make({ sourceId }), + source: NicovideoOriginalSourceDTO.make({ + sourceId, + }), }; }) satisfies QueryResolvers["fetchNicovideo"]; diff --git a/src/FetchExternal/fetchYoutube.graphql b/src/FetchExternal/fetchYoutube.graphql index 69267413..72a6aa33 100644 --- a/src/FetchExternal/fetchYoutube.graphql +++ b/src/FetchExternal/fetchYoutube.graphql @@ -11,9 +11,3 @@ input FetchYoutubeInput { type FetchYoutubePayload { source: YoutubeOriginalSource } - -type YoutubeOriginalSource { - sourceId: String! - url: String! - thumbnailUrl: String! -} diff --git a/src/FetchExternal/fetchYoutube.ts b/src/FetchExternal/fetchYoutube.ts index b35e61d4..7e55434d 100644 --- a/src/FetchExternal/fetchYoutube.ts +++ b/src/FetchExternal/fetchYoutube.ts @@ -2,25 +2,27 @@ import { GraphQLError } from "graphql"; import { QueryResolvers } from "../resolvers/graphql.js"; import { isValidYoutubeSourceId } from "../utils/isValidYoutubeSourceId.js"; +import { MkQueryResolver } from "../utils/MkResolver.js"; +import { YoutubeOriginalSourceDTO } from "./YoutubeOriginalSource.dto.js"; -export const resolverFetchYoutube = () => +export const resolverFetchYoutube: MkQueryResolver<"fetchYoutube"> = () => (async (_parent, { input: { sourceId } }) => { if (!isValidYoutubeSourceId(sourceId)) { throw new GraphQLError("Invalid sourceId"); } - const thumbnailUrl = new URL(`vi/${sourceId}/hqdefault.jpg`, "https://i.ytimg.com/"); - const ok = await fetch(thumbnailUrl.toString()).then((res) => res.ok); + const originalThumbnailUrl = new URL(`vi/${sourceId}/hqdefault.jpg`, "https://i.ytimg.com/"); + const ok = await fetch(originalThumbnailUrl.toString()).then((res) => res.ok); if (!ok) return { source: null }; const url = new URL("/watch", "https://www.youtube.com/"); url.searchParams.set("v", sourceId); return { - source: { + source: YoutubeOriginalSourceDTO.make({ url: url.toString(), sourceId, - thumbnailUrl: thumbnailUrl.toString(), - }, + originalThumbnailUrl: originalThumbnailUrl.toString(), + }), }; }) satisfies QueryResolvers["fetchYoutube"]; diff --git a/src/common.graphql b/src/common.graphql index 43b8e50b..7a8c726c 100644 --- a/src/common.graphql +++ b/src/common.graphql @@ -83,3 +83,11 @@ type MutationYoutubeRegistrationRequestNotFoundError { type MutationYoutubeRegistrationRequestAlreadyCheckedError { requestId: String! } + +enum FetchExternalSourceThumbnailScale { + "960 x 700" + LARGE + + "720 x 400" + OGP +} diff --git a/src/query.ts b/src/query.ts index b7635bef..a44026a6 100644 --- a/src/query.ts +++ b/src/query.ts @@ -75,7 +75,9 @@ export const resolveQuery = (deps: ResolverDeps) => fetchSoundcloud: mkFetchSoundcloudResolver(deps), fetchSoundcloudBySourceId: mkFetchSoundcloudBySourceIdResolver(deps), fetchSoundcloudByUrl: mkFetchSoundcloudByUrlResolver(deps), - fetchYoutube: resolverFetchYoutube(), + fetchYoutube: resolverFetchYoutube({ + ...deps, + }), findBilibiliMADSource: mkFindBilibiliMADSourceResolver(deps), findBilibiliRegistrationRequestBySourceId: mkFindBilibiliRegistrationRequestByUrlResolver(deps), findMadBySerial: resolverFindMadBySerial(deps), diff --git a/src/resolvers.ts b/src/resolvers.ts index a7a43eaf..67f69e1f 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -23,6 +23,7 @@ import { resolverBilibiliOriginalSourceTag } from "./FetchExternal/BilibiliOrigi import { mkNicovideoOriginalSourceResolver } from "./FetchExternal/NicovideoOriginalSource.resolver.js"; import { resolveNicovideoOriginalSourceTag } from "./FetchExternal/NicovideoOriginalSourceTag.resolver.js"; import { mkSoundcloudOriginalSourceResolver } from "./FetchExternal/SoundcloudOriginalSource.resolver.js"; +import { mkYoutubeOriginalSourceResolver } from "./FetchExternal/YoutubeOriginalSource.resolver.js"; import { resolveMutation } from "./mutation.js"; import { resolverNicovideoRegistrationRequest } from "./NicovideoRegistrationRequest/NicovideoRegistrationRequest.resolver.js"; import { resolverNicovideoRegistrationRequestAccepting } from "./NicovideoRegistrationRequest/NicovideoRegistrationRequestAccepting.resolver.js"; @@ -263,6 +264,7 @@ export const makeResolvers = (deps: ResolverDeps) => VideoTitleSetPrimaryEvent: resolveVideoTitleSetPrimaryEvent(deps), VideoTitleUnsetPrimaryEvent: resolveVideoTitleUnsetPrimaryEvent(deps), YoutubeMadRequestedTimelineEvent: mkYoutubeMadRequestedTimelineEventResolver(deps), + YoutubeOriginalSource: mkYoutubeOriginalSourceResolver(deps), YoutubeRegistrationRequest: resolverYoutubeRegistrationRequest(deps), YoutubeRegistrationRequestAcceptEvent: mkYoutubeRegistrationRequestAcceptEventResolver(deps), YoutubeRegistrationRequestAccepting: resolverYoutubeRegistrationRequestAccepting(deps),