From 5564695121c7f2adaeea00fb3b789fe3353d40d6 Mon Sep 17 00:00:00 2001 From: xpadev Date: Thu, 9 Nov 2023 12:05:07 +0900 Subject: [PATCH] [wip] feat/controller: support domand server --- electron/queue.ts | 1 + src/@types/niconico.d.ts | 105 ++++++++++------ src/@types/queue.d.ts | 18 ++- .../movie-picker/remote/delivery/delivery.tsx | 83 +++++++++++++ .../movie-picker/remote/delivery/index.tsx | 1 + .../movie-picker/remote/domand/domand.tsx | 81 +++++++++++++ .../movie-picker/remote/domand/index.tsx | 1 + .../remote/remote-movie-picker.tsx | 112 +++++++----------- src/controller/queue/MovieItem.tsx | 32 ++++- src/typeGuard.ts | 5 + src/util/niconico.ts | 41 ++++++- 11 files changed, 365 insertions(+), 115 deletions(-) create mode 100644 src/controller/movie-picker/remote/delivery/delivery.tsx create mode 100644 src/controller/movie-picker/remote/delivery/index.tsx create mode 100644 src/controller/movie-picker/remote/domand/domand.tsx create mode 100644 src/controller/movie-picker/remote/domand/index.tsx diff --git a/electron/queue.ts b/electron/queue.ts index 25794e9..191fd13 100644 --- a/electron/queue.ts +++ b/electron/queue.ts @@ -61,6 +61,7 @@ const startMovieDownload = async (): Promise => { try { await download( targetQueue.url, + //@ts-ignore targetQueue.format, targetQueue.path, (total, downloaded) => { diff --git a/src/@types/niconico.d.ts b/src/@types/niconico.d.ts index 0bfafe8..1af7527 100644 --- a/src/@types/niconico.d.ts +++ b/src/@types/niconico.d.ts @@ -1,45 +1,16 @@ -export type TWatchV3Metadata = { +export type TWatchV3Metadata = { meta: { status: 200; }; data: { comment: V3MetadataComment; media: { - delivery: { - recipeId: string; - encryption: null | { - encryptedKey: string; - keyUri: string; - }; - movie: { - contentId: string; - audios: V3MetadataAudioItem[]; - videos: V3MetadataVideoItem[]; - session: { - recipeId: string; - playerId: string; - videos: string[]; - audios: string[]; - movies: []; - protocols: ["http", "hls"] | ["hls"]; - AuthTypes: { [key in "http" | "hls"]: "ht2" }; - serviceUserId: string; - token: string; - signature: string; - contentId: string; - heartbeatLifetime: number; - contentKeyTimeout: number; - priority: number; - transferPresets: [] | ["standard2"]; - urls: { - url: string; - isWellKnownPort: boolean; - isSsl: boolean; - }[]; - }; - }; - trackingId: string; - }; + delivery: T extends "delivery" + ? V3MetadataDeliveryMedia + : V3MetadataDeliveryMedia | null; + domand: T extends "domand" + ? V3MetadataDomandMedia + : V3MetadataDomandMedia | null; }; video: { id: string; @@ -99,6 +70,68 @@ export type V3MetadataVideoItem = { }; }; +export type V3MetadataDeliveryMedia = { + recipeId: string; + encryption: null | { + encryptedKey: string; + keyUri: string; + }; + movie: { + contentId: string; + audios: V3MetadataAudioItem[]; + videos: V3MetadataVideoItem[]; + session: { + recipeId: string; + playerId: string; + videos: string[]; + audios: string[]; + movies: []; + protocols: ["http", "hls"] | ["hls"]; + AuthTypes: { [key in "http" | "hls"]: "ht2" }; + serviceUserId: string; + token: string; + signature: string; + contentId: string; + heartbeatLifetime: number; + contentKeyTimeout: number; + priority: number; + transferPresets: [] | ["standard2"]; + urls: { + url: string; + isWellKnownPort: boolean; + isSsl: boolean; + }[]; + }; + }; + trackingId: string; +}; + +export type V3MetadataDomandMedia = { + videos: V3MetadataDomandVideoItem[]; + audios: V3MetadataDomandAudioItem[]; +}; + +export type V3MetadataDomandVideoItem = { + id: string; + isAvailable: boolean; + label: string; + bitRate: number; + width: number; + height: number; + qualityLevel: number; + recommendedHighestAudioQualityLevel: number; +}; + +export type V3MetadataDomandAudioItem = { + id: string; + isAvailable: boolean; + bitRate: number; + samplingRate: number; + integratedLoudness: number; + truePeak: number; + qualityLevel: number; +}; + export type V3MetadataComment = { server: { url: string; diff --git a/src/@types/queue.d.ts b/src/@types/queue.d.ts index 4f8aa25..a1621d5 100644 --- a/src/@types/queue.d.ts +++ b/src/@types/queue.d.ts @@ -45,6 +45,22 @@ export type TMovieItemRemote = { ref: MovieQueue; }; +export type TRemoteServerType = "delivery" | "domand"; +export type TRemoteMovieItemFormat = TDomandFormat | TDeliveryFormat; + +export type TDeliveryFormat = { + type: "delivery"; + format: { + audio: string; + video: string; + }; +}; + +export type TDomandFormat = { + type: "domand"; + format: [string, string]; +}; + export type ConvertQueue = BaseQueue & { type: "convert"; comment: TCommentItem; @@ -65,7 +81,7 @@ export type ConvertQueue = BaseQueue & { export type MovieQueue = BaseQueue & { type: "movie"; url: NicoId; - format: NicovideoMovieFormat; + format: TRemoteMovieItemFormat; path: string; }; diff --git a/src/controller/movie-picker/remote/delivery/delivery.tsx b/src/controller/movie-picker/remote/delivery/delivery.tsx new file mode 100644 index 0000000..612cdda --- /dev/null +++ b/src/controller/movie-picker/remote/delivery/delivery.tsx @@ -0,0 +1,83 @@ +import { MenuItem, Select } from "@mui/material"; +import type { FC } from "react"; +import { useEffect, useState } from "react"; + +import type { TWatchV3Metadata } from "@/@types/niconico"; +import type { TDeliveryFormat } from "@/@types/queue"; +import { SelectField } from "@/components/SelectField"; +import Styles from "@/controller/movie/movie.module.scss"; +import { getDeliveryBestSegment } from "@/util/niconico"; + +type Props = { + metadata: TWatchV3Metadata<"delivery">; + onChange: (val: TDeliveryFormat | undefined) => void; +}; + +const DeliveryMoviePicker: FC = ({ metadata, onChange }) => { + const [selectedVideo, setSelectedVideo] = useState(""); + const [selectedAudio, setSelectedAudio] = useState(""); + useEffect(() => { + setSelectedAudio( + getDeliveryBestSegment(metadata.data.media.delivery.movie.audios).id, + ); + setSelectedVideo( + getDeliveryBestSegment(metadata.data.media.delivery.movie.videos).id, + ); + }, [metadata]); + useEffect(() => { + onChange({ + type: "delivery", + format: { + video: selectedVideo, + audio: selectedAudio, + }, + }); + }, [selectedAudio, selectedVideo]); + return ( + <> + + + + + + + + ); +}; + +export { DeliveryMoviePicker }; diff --git a/src/controller/movie-picker/remote/delivery/index.tsx b/src/controller/movie-picker/remote/delivery/index.tsx new file mode 100644 index 0000000..2b23bcb --- /dev/null +++ b/src/controller/movie-picker/remote/delivery/index.tsx @@ -0,0 +1 @@ +export * from "./delivery"; diff --git a/src/controller/movie-picker/remote/domand/domand.tsx b/src/controller/movie-picker/remote/domand/domand.tsx new file mode 100644 index 0000000..1c32a59 --- /dev/null +++ b/src/controller/movie-picker/remote/domand/domand.tsx @@ -0,0 +1,81 @@ +import { MenuItem, Select } from "@mui/material"; +import type { FC } from "react"; +import { useEffect, useState } from "react"; + +import type { TWatchV3Metadata } from "@/@types/niconico"; +import type { TDomandFormat } from "@/@types/queue"; +import { SelectField } from "@/components/SelectField"; +import Styles from "@/controller/movie/movie.module.scss"; +import { getDomandBestSegment } from "@/util/niconico"; + +type Props = { + metadata: TWatchV3Metadata<"domand">; + onChange: (val: TDomandFormat | undefined) => void; +}; + +const DomandMoviePicker: FC = ({ metadata, onChange }) => { + const [selectedVideo, setSelectedVideo] = useState(""); + const [selectedAudio, setSelectedAudio] = useState(""); + useEffect(() => { + setSelectedAudio( + getDomandBestSegment(metadata.data.media.domand.audios).id, + ); + setSelectedVideo( + getDomandBestSegment(metadata.data.media.domand.videos).id, + ); + }, [metadata]); + + useEffect(() => { + onChange({ + type: "domand", + format: [selectedVideo, selectedAudio], + }); + }, [selectedAudio, selectedVideo]); + return ( + <> + + + + + + + + ); +}; + +export { DomandMoviePicker }; diff --git a/src/controller/movie-picker/remote/domand/index.tsx b/src/controller/movie-picker/remote/domand/index.tsx new file mode 100644 index 0000000..1e30272 --- /dev/null +++ b/src/controller/movie-picker/remote/domand/index.tsx @@ -0,0 +1 @@ +export * from "./domand"; diff --git a/src/controller/movie-picker/remote/remote-movie-picker.tsx b/src/controller/movie-picker/remote/remote-movie-picker.tsx index 2c7bfa8..e3897f9 100644 --- a/src/controller/movie-picker/remote/remote-movie-picker.tsx +++ b/src/controller/movie-picker/remote/remote-movie-picker.tsx @@ -1,18 +1,17 @@ -import { MenuItem, Select, TextField } from "@mui/material"; +import { FormControlLabel, Radio, RadioGroup, TextField } from "@mui/material"; import Button from "@mui/material/Button"; import { useSetAtom } from "jotai"; import type { ChangeEvent, FC } from "react"; import { useState } from "react"; -import type { - TWatchV3Metadata, - V3MetadataAudioItem, - V3MetadataVideoItem, -} from "@/@types/niconico"; -import type { TMovieItemRemote } from "@/@types/queue"; -import { SelectField } from "@/components/SelectField"; +import type { TWatchV3Metadata } from "@/@types/niconico"; +import type { TMovieItemRemote, TRemoteMovieItemFormat } from "@/@types/queue"; +import type { TRemoteServerType } from "@/@types/queue"; import { isLoadingAtom, messageAtom } from "@/controller/atoms"; import Styles from "@/controller/movie/movie.module.scss"; +import { DeliveryMoviePicker } from "@/controller/movie-picker/remote/delivery"; +import { DomandMoviePicker } from "@/controller/movie-picker/remote/domand"; +import { typeGuard } from "@/typeGuard"; import { getNicoId, isNicovideoUrl } from "@/util/niconico"; import { uuid } from "@/util/uuid"; @@ -23,8 +22,8 @@ type Props = { const RemoteMoviePicker: FC = ({ onChange }) => { const [url, setUrl] = useState(""); const [metadata, setMetadata] = useState(); - const [selectedVideo, setSelectedVideo] = useState(""); - const [selectedAudio, setSelectedAudio] = useState(""); + const [mediaServer, setMediaServer] = useState("delivery"); + const [format, setFormat] = useState(); const setMessage = useSetAtom(messageAtom); const setIsLoading = useSetAtom(isLoadingAtom); const onUrlChange = (e: ChangeEvent): void => { @@ -64,13 +63,8 @@ const RemoteMoviePicker: FC = ({ onChange }) => { }); return; } + setMediaServer(targetMetadata.data.media.domand ? "domand" : "delivery"); setMetadata(targetMetadata); - setSelectedAudio( - getBestSegment(targetMetadata.data.media.delivery.movie.audios).id, - ); - setSelectedVideo( - getBestSegment(targetMetadata.data.media.delivery.movie.videos).id, - ); })(); }; const onClick = (): void => { @@ -84,7 +78,7 @@ const RemoteMoviePicker: FC = ({ onChange }) => { }); return; } - if (!selectedVideo || !selectedAudio || !metadata) { + if (!format || !metadata) { return; } setIsLoading(true); @@ -111,7 +105,7 @@ const RemoteMoviePicker: FC = ({ onChange }) => { status: "queued", type: "movie", url: nicoId, - format: { audio: selectedAudio, video: selectedVideo }, + format: format, path: output, progress: 0, }, @@ -132,47 +126,34 @@ const RemoteMoviePicker: FC = ({ onChange }) => { /> {metadata && ( <> - - - - - - + + setMediaServer(e.target.value as TRemoteServerType) + } + row + > + } + label={"delivery"} + disabled={!metadata.data.media.delivery} + /> + } + disabled={!metadata.data.media.domand} + label={"domand"} + /> + + {mediaServer === "delivery" && + typeGuard.controller.v3Delivery(metadata) && ( + + )} + {mediaServer === "domand" && + typeGuard.controller.v3Domand(metadata) && ( + + )} @@ -182,17 +163,4 @@ const RemoteMoviePicker: FC = ({ onChange }) => { ); }; -function getBestSegment( - input: T[], -): T { - let bestItem = input[0]; - for (const item of input) { - if (!item.isAvailable) continue; - if (bestItem.metadata.bitrate < item.metadata.bitrate) { - bestItem = item; - } - } - return bestItem; -} - export { RemoteMoviePicker }; diff --git a/src/controller/queue/MovieItem.tsx b/src/controller/queue/MovieItem.tsx index d571319..d73cf3d 100644 --- a/src/controller/queue/MovieItem.tsx +++ b/src/controller/queue/MovieItem.tsx @@ -1,7 +1,7 @@ import type { FC } from "react"; import { useMemo } from "react"; -import type { MovieQueue } from "@/@types/queue"; +import type { MovieQueue, TRemoteMovieItemFormat } from "@/@types/queue"; import { ProgressDisplay } from "@/controller/queue/ProgressDisplay"; import Styles from "./ConvertItem.module.scss"; @@ -18,8 +18,7 @@ const MovieItem: FC = ({ queue, className }) => { return (

id: {url}

-

video: {queue.format.video.slice(8)}

-

audio: {queue.format.audio.slice(8)}

+

output: {outputName}

status: {queue.status}

@@ -29,8 +28,7 @@ const MovieItem: FC = ({ queue, className }) => { return (

id: {url}

-

video: {queue.format.video.slice(8)}

-

audio: {queue.format.audio.slice(8)}

+

path: {outputName}

status: processing

@@ -40,4 +38,28 @@ const MovieItem: FC = ({ queue, className }) => { ); }, [queue]); }; + +type FormatDisplayProps = { + format: TRemoteMovieItemFormat; +}; + +const FormatDisplay: FC = ({ format }) => { + return ( + <> + {format.type === "delivery" && ( + <> +

video: {format.format.video.slice(8)}

+

audio: {format.format.audio.slice(8)}

+ + )} + {format.type === "domand" && ( + <> +

video: {format.format[0]}

+

audio: {format.format[1]}

+ + )} + + ); +}; + export { MovieItem }; diff --git a/src/typeGuard.ts b/src/typeGuard.ts index 3b02208..87c9737 100644 --- a/src/typeGuard.ts +++ b/src/typeGuard.ts @@ -1,3 +1,4 @@ +import type { TWatchV3Metadata } from "@/@types/niconico"; import type { ApiResponseDownloadProgress } from "@/@types/response.binaryDownloader"; import type { ApiResponseEnd, @@ -26,6 +27,10 @@ const typeGuard = { typeof i === "object" && (i as ApiResponseEnd).type === "end", message: (i: unknown): i is ApiResponseMessage => typeof i === "object" && (i as ApiResponseMessage).type === "message", + v3Delivery: (i: unknown): i is TWatchV3Metadata<"delivery"> => + typeof i === "object" && !!(i as TWatchV3Metadata).data.media.delivery, + v3Domand: (i: unknown): i is TWatchV3Metadata<"domand"> => + typeof i === "object" && !!(i as TWatchV3Metadata).data.media.domand, }, renderer: { progress: (i: unknown): i is ApiResponseProgress => diff --git a/src/util/niconico.ts b/src/util/niconico.ts index b4e0fd7..8bf4dd8 100644 --- a/src/util/niconico.ts +++ b/src/util/niconico.ts @@ -1,4 +1,10 @@ import type { NicoId } from "@/@types/brand"; +import type { + V3MetadataAudioItem, + V3MetadataDomandAudioItem, + V3MetadataDomandVideoItem, + V3MetadataVideoItem, +} from "@/@types/niconico"; const isNicovideoUrl = (url: string): boolean => { return !!url.match( @@ -13,4 +19,37 @@ const getNicoId = (url: string): NicoId | undefined => { return match[1] as NicoId; }; -export { getNicoId, isNicovideoUrl }; +function getDeliveryBestSegment< + T extends V3MetadataAudioItem | V3MetadataVideoItem, +>(input: T[]): T { + let bestItem = input[0]; + for (const item of input) { + if (!item.isAvailable) continue; + if (bestItem.metadata.bitrate < item.metadata.bitrate) { + bestItem = item; + } + } + return bestItem; +} + +const getDomandBestSegment = < + T extends V3MetadataDomandAudioItem | V3MetadataDomandVideoItem, +>( + input: T[], +): T => { + let bestItem = input[0]; + for (const item of input) { + if (!item.isAvailable) continue; + if (bestItem.bitRate < item.bitRate) { + bestItem = item; + } + } + return bestItem; +}; + +export { + getDeliveryBestSegment, + getDomandBestSegment, + getNicoId, + isNicovideoUrl, +};