Compare commits

..
10 changed files with 241 additions and 285 deletions

No files matched your search

+8 -7
View File
@@ -25,6 +25,7 @@ export default (_: ConfigContext): ExpoConfig => ({
supportsTablet: true,
config: { usesNonExemptEncryption: false },
infoPlist: {
UIBackgroundModes: "audio",
UIViewControllerBasedStatusBarAppearance: true,
CFBundleShortVersionString: "$(MARKETING_VERSION)",
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)",
@@ -65,13 +66,13 @@ export default (_: ConfigContext): ExpoConfig => ({
},
},
],
[
"expo-media-control",
{
enableBackgroundAudio: true,
audioSessionCategory: "playback",
},
],
// [
// "expo-media-control",
// {
// enableBackgroundAudio: true,
// audioSessionCategory: "playback",
// },
// ],
[
"expo-asset",
{
+2 -3
View File
@@ -15,7 +15,7 @@
"@gorhom/bottom-sheet": "^5.2.14",
"@kesha-antonov/react-native-background-downloader": "^4.5.6",
"@shopify/flash-list": "2.3.2",
"@shopify/react-native-skia": "2.6.9",
"@shopify/react-native-skia": "2.9.0",
"@tanstack/react-query": "^5.100.5",
"api": "workspace:^",
"babel-preset-expo": "^56.0.17",
@@ -33,8 +33,6 @@
"expo-image": "^56.0.11",
"expo-linear-gradient": "^56.0.4",
"expo-linking": "~56.0.15",
"expo-media-control": "patch:expo-media-control@npm%3A1.0.12#~/.yarn/patches/expo-media-control-npm-1.0.12-d7bddfea85.patch",
"expo-network": "^57.0.1",
"expo-router": "~56.2.14",
"expo-screen-orientation": "~56.0.5",
"expo-splash-screen": "~56.0.12",
@@ -57,6 +55,7 @@
"react-native-edge-to-edge": "^1.8.1",
"react-native-gesture-handler": "~2.31.2",
"react-native-keyboard-controller": "1.22.2",
"react-native-media-notification": "0.5.0-beta.6",
"react-native-mmkv": "4.3.2",
"react-native-nitro-modules": "0.36.4",
"react-native-reanimated": "~4.5.1",
@@ -1,14 +1,7 @@
import {
Command,
MediaControl,
type MediaControlEvent,
PlaybackState,
} from "expo-media-control";
import { getNetworkStateAsync, NetworkStateType } from "expo-network";
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { useEffect, useRef, useState } from "react";
import { Platform } from "react-native";
import Toast from "react-native-toast-message";
import * as MediaControls from "react-native-media-notification";
import uuid from "react-native-uuid";
import {
type onLoadData,
@@ -36,7 +29,6 @@ import {
queuePrefetchCountAtom,
useDownloadManager,
} from "~/downloads";
import { streamingPreferenceAtom } from "~/state/streaming";
import {
currentTrackAtom,
durationAtom,
@@ -81,7 +73,6 @@ export const PlayerContext = () => {
const { data: settings } = useQuery(getSettings);
const isSwitchingTrack = useRef(false);
const controlsRegistered = useRef(false);
const controlsListener = useRef<(() => void) | null>(null);
const ready = useRef(false);
const { download } = useDownloadManager();
@@ -96,90 +87,53 @@ export const PlayerContext = () => {
(controlsRegistered.current &&
ready.current &&
!isSwitchingTrack.current)
)
await MediaControl.updatePlaybackState(
(isPlaying ?? store.get(isPlayingAtom))
? PlaybackState.PLAYING
: PlaybackState.PAUSED,
progress ?? store.get(progressAtom),
1,
);
) {
await MediaControls.updateMetadata({
isPlaying,
position: progress,
});
}
};
const enableControls = async (enable: boolean) => {
if (enable === false) {
await MediaControl.removeAllListeners();
await MediaControl.disableMediaControls();
controlsListener.current?.();
controlsListener.current = null;
if (controlsRegistered.current === false) {
return;
}
controlsRegistered.current = false;
MediaControls.removeAllListeners();
await MediaControls.stopMediaNotification();
return;
}
if (controlsRegistered.current === false) {
await MediaControl.enableMediaControls({
capabilities: [
Command.PLAY,
Command.PAUSE,
Command.STOP,
Command.NEXT_TRACK,
Command.PREVIOUS_TRACK,
Command.SEEK,
],
compactCapabilities: [
Command.PREVIOUS_TRACK,
Command.PLAY,
Command.NEXT_TRACK,
],
notification: { showWhenClosed: true },
ios: { skipInterval: 15 },
android: { skipInterval: 15 },
// MediaControls.enableBackgroundMode(true);
// MediaControls.enableAudioInterruption(true);
MediaControls.setControlEnabled("play", true);
MediaControls.setControlEnabled("pause", true);
MediaControls.setControlEnabled("skipToNext", true);
MediaControls.setControlEnabled("skipToPrevious", true);
MediaControls.setControlEnabled("stop", true);
MediaControls.setControlEnabled("shuffle", false);
MediaControls.setControlEnabled("repeatMode", false);
MediaControls.addEventListener("play", () => store.set(playAtom));
MediaControls.addEventListener("pause", () => store.set(pauseAtom));
MediaControls.addEventListener("seek", (payload) => {
if (payload?.seekPosition !== undefined)
store.set(requestedProgressAtom, payload.seekPosition);
});
controlsRegistered.current = true;
controlsListener.current = MediaControl.addListener(
(event: MediaControlEvent) => {
const progress = store.get(progressAtom);
switch (event.command) {
case Command.PLAY:
playerRef.current?.play();
break;
case Command.PAUSE:
playerRef.current?.pause();
break;
case Command.STOP:
MediaControl.updatePlaybackState(
PlaybackState.STOPPED,
0,
1,
);
store.set(emptyPlaylistAtom);
break;
case Command.NEXT_TRACK:
store.set(skipTrackAtom, queryClient);
break;
case Command.PREVIOUS_TRACK:
store.set(rewindTrackAtom);
break;
case Command.SEEK:
store.set(
requestedProgressAtom,
event.data.position,
);
break;
case Command.SKIP_FORWARD:
store.set(
requestedProgressAtom,
progress + (event.data?.interval || 15),
);
break;
case Command.SKIP_BACKWARD:
store.set(
requestedProgressAtom,
progress - (event.data?.interval || 15),
);
break;
}
},
MediaControls.addEventListener("skipToNext", () =>
store.set(skipTrackAtom, queryClient),
);
MediaControls.addEventListener("skipToPrevious", () =>
store.set(rewindTrackAtom),
);
MediaControls.addEventListener("stop", () =>
store.set(emptyPlaylistAtom),
);
//NOTE: RepeatMode button is broken on android
controlsRegistered.current = true;
}
};
@@ -194,8 +148,8 @@ export const PlayerContext = () => {
prefetchCount + 1 + cursor, // NOTE: add one to include current song
);
for (const track of queue) {
// if (track.track.type === "Audio")
// download(track.track.sourceFileId);
if (track.track.type === "Audio")
download(track.track.sourceFileId);
}
}, [playlist, cursor]);
@@ -240,14 +194,12 @@ export const PlayerContext = () => {
return;
}
if (playerRef.current === null) {
mkSource(queryClient, currentTrack, "direct").then((source) => {
mkSource(queryClient, currentTrack).then((source) => {
playerRef.current = new VideoPlayer(source);
playerRef.current.mixAudioMode = "doNotMix";
playerRef.current.ignoreSilentSwitchMode = "ignore";
// NOTE: On ios, When the player is paused, the 'focus' is lost and the media controls are flushed (cannot press play again + no metadata)
// Setting this opt to true preserves the focus.
playerRef.current.showNotificationControls =
Platform.OS === "ios";
playerRef.current.playInBackground = true;
playerRef.current.addEventListener(
"onPlaybackStateChange",
@@ -289,30 +241,30 @@ export const PlayerContext = () => {
ready.current = false;
enableControls(true).then(async () => {
const currentTrack = store.get(currentTrackAtom)!;
await MediaControl.updatePlaybackState(
PlaybackState.PLAYING,
0,
1,
const cursor = store.get(cursorAtom);
const playlist = store.get(playlistAtom);
MediaControls.setControlEnabled(
"skipToNext",
cursor < playlist.length - 1 && cursor >= 0,
);
await MediaControl.updateMetadata({
trackNumber: store.get(cursorAtom) + 1,
albumTrackCount: store.get(playlistAtom).length,
MediaControls.setControlEnabled(
"skipToPrevious",
cursor > 0,
);
await MediaControls.updateMetadata({
title: currentTrack.track.name,
elapsedTime: 0,
position: 0,
duration: duration ?? 1,
artist: formatArtists(
currentTrack.artist,
currentTrack.featuring,
),
artwork: currentTrack.track.illustration
? {
uri: getAPI().getIllustrationURL(
currentTrack.track.illustration
.url,
"original",
true,
),
}
? getAPI().getIllustrationURL(
currentTrack.track.illustration.url,
"original",
true,
)
: undefined,
});
ready.current = true;
@@ -339,7 +291,7 @@ export const PlayerContext = () => {
isSwitchingTrack.current = true;
ready.current = false;
playerRef.current!.pause();
mkSource(queryClient, currentTrack, "direct").then((source) => {
mkSource(queryClient, currentTrack).then((source) => {
playerRef.current
?.replaceSourceAsync(source)
.then(() => {
@@ -369,11 +321,7 @@ export const PlayerContext = () => {
}
const timestamp = playerRef.current?.currentTime;
mkSource(
queryClient,
currentTrack,
isHLS ? "transcoding" : "direct",
).then((source) =>
mkSource(queryClient, currentTrack, isHLS).then((source) =>
playerRef.current?.replaceSourceAsync(source).then(() => {
playerRef.current!.play();
if (isHLS) {
@@ -415,55 +363,40 @@ const clientId = uuid.v4();
const mkSource = async (
queryClient: QueryClient,
st: TrackState,
method: "direct" | "transcoding",
useHLS: boolean = false,
) => {
const api = getAPI();
const [dlStatus, localPath] = await getDownloadStatus(
st.track.sourceFileId,
);
const streamingPreference = store.get(streamingPreferenceAtom);
console.log(streamingPreference);
const streamingQuality =
dlStatus === "downloaded"
? null
: await getNetworkStateAsync().then((st) =>
st.type === NetworkStateType.WIFI
? streamingPreference.wifi
: streamingPreference.cellular,
);
const shouldDownload = method === "direct" && st.track.type === "Audio";
// if (!shouldDownload || dlStatus !== "downloaded") {
if (shouldDownload) {
// await downloadFile(queryClient)(st.track.sourceFileId);
const shouldDownload = !useHLS && st.track.type === "Audio";
if (!shouldDownload || dlStatus !== "downloaded") {
if (shouldDownload) {
await downloadFile(queryClient)(st.track.sourceFileId);
}
return _mkSource(st, api, useHLS);
}
const res = _mkSource(st, api, "transcoding");
console.log(res);
if (streamingQuality) {
Toast.show({
text1: "Playing in quality " + streamingQuality?.audio,
});
res.uri = res.uri.replace("/128k/", `/${streamingQuality?.audio}/`);
} else {
Toast.show({ text1: "Streaming quality unspecified" });
}
return res;
// }
// return { uri: localPath };
return _mkSource(st, api, false, localPath);
};
const _mkSource = (
{ track }: TrackState,
api: API,
method: "direct" | "transcoding",
useTranscoding = false,
) =>
({
uri:
method === "transcoding"
? api.getTranscodeStreamURL(track.sourceFileId, track.type)
: api.getDirectStreamURL(track.sourceFileId),
headers: {
...api.getAuthHeaders(),
"X-CLIENT-ID": clientId,
},
}) satisfies VideoConfig;
localPath?: string,
): VideoConfig => ({
uri:
localPath ??
(useTranscoding
? api.getTranscodeStreamURL(track.sourceFileId, track.type)
: api.getDirectStreamURL(track.sourceFileId)),
headers: {
...api.getAuthHeaders(),
...(useTranscoding
? {
"X-CLIENT-ID": clientId,
}
: {}),
},
});
@@ -0,0 +1 @@
export const useMediaControls = () => {};
@@ -0,0 +1,6 @@
export type MediaControls = {
updateMediaControlsState: (
isPlaying?: boolean,
progress?: number,
) => Promise<void>;
};
-79
View File
@@ -1,79 +0,0 @@
import { atom } from "jotai";
import * as yup from "yup";
import { storage } from "~/utils/storage";
export const AudioQualities = [
"128k",
"192k",
"256k",
"320k",
"512k",
"original",
] as const;
type AudioQuality = (typeof AudioQualities)[number];
export const VideoQualities = [
"240p",
"480p",
"720p",
"1080p",
"original",
] as const;
type VideoQuality = (typeof VideoQualities)[number];
const StreamingQuality = yup.object({
audio: yup.mixed<AudioQuality>().oneOf(AudioQualities).default("320k"),
video: yup.mixed<VideoQuality>().oneOf(VideoQualities).default("original"),
});
type StreamingQuality = yup.InferType<typeof StreamingQuality>;
const StreamingPreferences = yup.object({
wifi: StreamingQuality.default({
audio: "320k",
video: "1080p",
} satisfies StreamingQuality).required(),
cellular: StreamingQuality.default({
audio: "128k",
video: "720p",
} satisfies StreamingQuality).required(),
download: StreamingQuality.default({
audio: "256k",
video: "720p",
} satisfies StreamingQuality).required(),
});
type StreamingPreferences = yup.InferType<typeof StreamingPreferences>;
export const StreamingPreferenceKey = "streaming-preferences";
const readStreamingPreferences = (): StreamingPreferences => {
const stringPrefs = storage.getString(StreamingPreferenceKey);
if (!stringPrefs) {
return StreamingPreferences.getDefault();
}
try {
const prefs = StreamingPreferences.validateSync(
JSON.parse(stringPrefs),
);
return prefs;
} catch (e) {
// biome-ignore lint/suspicious/noConsole: For debug
console.warn(
`An error occured when reading streaming preferences: ${e}`,
);
return StreamingPreferences.getDefault();
}
};
export const streamingPreferenceAtom = atom<
StreamingPreferences,
[StreamingPreferences],
void
>(
(get) => get(_streamingPreferenceAtom),
(_, set, newPref) => {
// TODO: Validate?
storage.set(StreamingPreferenceKey, JSON.stringify(newPref));
set(_streamingPreferenceAtom, newPref);
},
);
const _streamingPreferenceAtom = atom(readStreamingPreferences());
+114 -41
View File
@@ -3929,9 +3929,9 @@ __metadata:
languageName: node
linkType: hard
"@shopify/react-native-skia@npm:2.6.9":
version: 2.6.9
resolution: "@shopify/react-native-skia@npm:2.6.9"
"@shopify/react-native-skia@npm:2.9.0":
version: 2.9.0
resolution: "@shopify/react-native-skia@npm:2.9.0"
dependencies:
canvaskit-wasm: "npm:0.41.0"
react-native-skia-android: "npm:150.0.0"
@@ -3950,7 +3950,7 @@ __metadata:
optional: true
bin:
setup-skia-web: scripts/setup-canvaskit.js
checksum: 10c0/89f5caba17ae56cb44f3916663d0a70616c02e9681dccc891dda809d53608cb1a17370eca99614168412e7158e4da92eaa72ad8c7b757dbf90f365448493434a
checksum: 10c0/1f4109b3c26dc4b4270af7851cf5be2ce725eac9021409feb3a77ec2bc5c3a4e68277f8e1e64aa37a958337f2e7258a3b7c6a5855b76f5823376bc0063fba805
languageName: node
linkType: hard
@@ -4755,7 +4755,7 @@ __metadata:
languageName: node
linkType: hard
"asap@npm:~2.0.6":
"asap@npm:~2.0.3, asap@npm:~2.0.6":
version: 2.0.6
resolution: "asap@npm:2.0.6"
checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d
@@ -5540,6 +5540,15 @@ __metadata:
languageName: node
linkType: hard
"cross-fetch@npm:^3.1.5":
version: 3.2.0
resolution: "cross-fetch@npm:3.2.0"
dependencies:
node-fetch: "npm:^2.7.0"
checksum: 10c0/d8596adf0269130098a676f6739a0922f3cc7b71cc89729925411ebe851a87026171c82ea89154c4811c9867c01c44793205a52e618ce2684650218c7fbeeb9f
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
@@ -6283,28 +6292,6 @@ __metadata:
languageName: node
linkType: hard
"expo-media-control@npm:1.0.12":
version: 1.0.12
resolution: "expo-media-control@npm:1.0.12"
peerDependencies:
expo: "*"
react: "*"
react-native: "*"
checksum: 10c0/02acf4a8eeb1075f2ee907fed30d14fb8e8c82b7e8e5ad02227f6e9b881be652ccbd5c5179d83defd77571e2e6a1145f2ad6e0324568471c98ac194c6a8beff1
languageName: node
linkType: hard
"expo-media-control@patch:expo-media-control@npm%3A1.0.12#~/.yarn/patches/expo-media-control-npm-1.0.12-d7bddfea85.patch":
version: 1.0.12
resolution: "expo-media-control@patch:expo-media-control@npm%3A1.0.12#~/.yarn/patches/expo-media-control-npm-1.0.12-d7bddfea85.patch::version=1.0.12&hash=3733d9"
peerDependencies:
expo: "*"
react: "*"
react-native: "*"
checksum: 10c0/6eea48dda39a59bffb8c1ce1de766fdd40dc2c570cbe9d36139e81dc62d277c33c9bb09d111cd30ac7e3c8b5e490e8a98e8966bb6ec6e3512d2d6401ce3c77ad
languageName: node
linkType: hard
"expo-modules-autolinking@npm:~56.0.19":
version: 56.0.19
resolution: "expo-modules-autolinking@npm:56.0.19"
@@ -6346,16 +6333,6 @@ __metadata:
languageName: node
linkType: hard
"expo-network@npm:^57.0.1":
version: 57.0.1
resolution: "expo-network@npm:57.0.1"
peerDependencies:
expo: "*"
react: "*"
checksum: 10c0/15e2b59a88a500fc803b296fa80988e39659ed618959fa02b370c8b9a70db968e21a62de350a1998ece06819b83c49e8c59ea32fe5d132b325640c98a544a85f
languageName: node
linkType: hard
"expo-router@npm:~56.2.14":
version: 56.2.14
resolution: "expo-router@npm:56.2.14"
@@ -6614,6 +6591,37 @@ __metadata:
languageName: node
linkType: hard
"fbemitter@npm:^3.0.0":
version: 3.0.0
resolution: "fbemitter@npm:3.0.0"
dependencies:
fbjs: "npm:^3.0.0"
checksum: 10c0/f130dd8e15dc3fc6709a26586b7a589cd994e1d1024b624f2cc8ef1b12401536a94bb30038e68150a24f9ba18863e9a3fe87941ade2c87667bfbd17f4848d5c7
languageName: node
linkType: hard
"fbjs-css-vars@npm:^1.0.0":
version: 1.0.2
resolution: "fbjs-css-vars@npm:1.0.2"
checksum: 10c0/dfb64116b125a64abecca9e31477b5edb9a2332c5ffe74326fe36e0a72eef7fc8a49b86adf36c2c293078d79f4524f35e80f5e62546395f53fb7c9e69821f54f
languageName: node
linkType: hard
"fbjs@npm:^3.0.0":
version: 3.0.5
resolution: "fbjs@npm:3.0.5"
dependencies:
cross-fetch: "npm:^3.1.5"
fbjs-css-vars: "npm:^1.0.0"
loose-envify: "npm:^1.0.0"
object-assign: "npm:^4.1.0"
promise: "npm:^7.1.1"
setimmediate: "npm:^1.0.5"
ua-parser-js: "npm:^1.0.35"
checksum: 10c0/66d0a2fc9a774f9066e35ac2ac4bf1245931d27f3ac287c7d47e6aa1fc152b243c2109743eb8f65341e025621fb51a12038fadb9fd8fda2e3ddae04ebab06f91
languageName: node
linkType: hard
"fdir@npm:^6.5.0":
version: 6.5.0
resolution: "fdir@npm:6.5.0"
@@ -8408,7 +8416,7 @@ __metadata:
"@gorhom/bottom-sheet": "npm:^5.2.14"
"@kesha-antonov/react-native-background-downloader": "npm:^4.5.6"
"@shopify/flash-list": "npm:2.3.2"
"@shopify/react-native-skia": "npm:2.6.9"
"@shopify/react-native-skia": "npm:2.9.0"
"@tanstack/react-query": "npm:^5.100.5"
"@types/react": "npm:~19.1.10"
"@types/react-native-base64": "npm:^0"
@@ -8428,8 +8436,6 @@ __metadata:
expo-image: "npm:^56.0.11"
expo-linear-gradient: "npm:^56.0.4"
expo-linking: "npm:~56.0.15"
expo-media-control: "patch:expo-media-control@npm%3A1.0.12#~/.yarn/patches/expo-media-control-npm-1.0.12-d7bddfea85.patch"
expo-network: "npm:^57.0.1"
expo-router: "npm:~56.2.14"
expo-screen-orientation: "npm:~56.0.5"
expo-splash-screen: "npm:~56.0.12"
@@ -8452,6 +8458,7 @@ __metadata:
react-native-edge-to-edge: "npm:^1.8.1"
react-native-gesture-handler: "npm:~2.31.2"
react-native-keyboard-controller: "npm:1.22.2"
react-native-media-notification: "npm:0.5.0-beta.6"
react-native-mmkv: "npm:4.3.2"
react-native-nitro-modules: "npm:0.36.4"
react-native-reanimated: "npm:~4.5.1"
@@ -8668,6 +8675,20 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.7.0":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
languageName: node
linkType: hard
"node-forge@npm:^1.3.3":
version: 1.3.3
resolution: "node-forge@npm:1.3.3"
@@ -9102,6 +9123,15 @@ __metadata:
languageName: node
linkType: hard
"promise@npm:^7.1.1":
version: 7.3.1
resolution: "promise@npm:7.3.1"
dependencies:
asap: "npm:~2.0.3"
checksum: 10c0/742e5c0cc646af1f0746963b8776299701ad561ce2c70b49365d62c8db8ea3681b0a1bf0d4e2fe07910bf72f02d39e51e8e73dc8d7503c3501206ac908be107f
languageName: node
linkType: hard
"promise@npm:^8.3.0":
version: 8.3.0
resolution: "promise@npm:8.3.0"
@@ -9460,6 +9490,18 @@ __metadata:
languageName: node
linkType: hard
"react-native-media-notification@npm:0.5.0-beta.6":
version: 0.5.0-beta.6
resolution: "react-native-media-notification@npm:0.5.0-beta.6"
dependencies:
fbemitter: "npm:^3.0.0"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10c0/0cdc03edfeeff43eb220b9ec01bf3064337b05a544be8f7d2a645cc68ecbb285441b8d20652a5033da033c400b1d76003a50ff9c9c19aaa09bc0e4bb75e3d2a3
languageName: node
linkType: hard
"react-native-mmkv@npm:4.3.2":
version: 4.3.2
resolution: "react-native-mmkv@npm:4.3.2"
@@ -10335,6 +10377,13 @@ __metadata:
languageName: node
linkType: hard
"setimmediate@npm:^1.0.5":
version: 1.0.5
resolution: "setimmediate@npm:1.0.5"
checksum: 10c0/5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49
languageName: node
linkType: hard
"setprototypeof@npm:1.2.0":
version: 1.2.0
resolution: "setprototypeof@npm:1.2.0"
@@ -11060,6 +11109,13 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11
languageName: node
linkType: hard
"ts-easing@npm:^0.2.0":
version: 0.2.0
resolution: "ts-easing@npm:0.2.0"
@@ -11147,7 +11203,7 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:^1.0.37":
"ua-parser-js@npm:^1.0.35, ua-parser-js@npm:^1.0.37":
version: 1.0.41
resolution: "ua-parser-js@npm:1.0.41"
bin:
@@ -11595,6 +11651,13 @@ __metadata:
languageName: unknown
linkType: soft
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db
languageName: node
linkType: hard
"whatwg-fetch@npm:^3.0.0":
version: 3.6.20
resolution: "whatwg-fetch@npm:3.6.20"
@@ -11609,6 +11672,16 @@ __metadata:
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
dependencies:
tr46: "npm:~0.0.3"
webidl-conversions: "npm:^3.0.0"
checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5
languageName: node
linkType: hard
"which@npm:^2.0.1":
version: 2.0.2
resolution: "which@npm:2.0.2"
+3
View File
@@ -53,6 +53,9 @@ func CollectTags(probeData *ffprobe.ProbeData) ffprobe.Tags {
// Tags are attached to the audio stream,
// Not the 'Format'
for _, s := range probeData.Streams {
if s.CodecType == "video" {
continue // NOTE: See #1961
}
for k, v := range s.TagList {
tags[strings.ToLower(k)] = v
}
+11
View File
@@ -244,6 +244,11 @@ describe("Parser Service", () => {
"Me Against the Music",
["Madonna"],
],
[
"Me Against the Music (ft. Madonna)",
"Me Against the Music",
["Madonna"],
],
[
"Me Against the Music feat. Madonna",
@@ -251,6 +256,12 @@ describe("Parser Service", () => {
["Madonna"],
],
[
"Me Against the Music ft. Madonna",
"Me Against the Music",
["Madonna"],
],
[
"Me Against the Music (featuring Madonna)",
"Me Against the Music",
+12 -4
View File
@@ -205,14 +205,14 @@ export default class ParserService {
const [sstart, strippedGroup, ssend] =
this.stripGroupDelimiters(group);
let featureSubGroup = strippedGroup.match(
/(^with|(feat(uring|\.)?))\s+(?<artists>.*)$/i,
/(^with|(ft\.?|feat(uring|\.)?))\s+(?<artists>.*)$/i,
);
let artistGroupIndex = 4;
if (!sstart && !ssend) {
// If there is no delimiters
featureSubGroup = strippedGroup.match(
/(feat(uring|\.)?)\s+(?<artists>.*)$/i,
/(ft\.?|feat(uring|\.)?)\s+(?<artists>.*)$/i,
);
artistGroupIndex = 3;
}
@@ -323,7 +323,11 @@ export default class ParserService {
.map((ext) => ext.toLowerCase())
.filter(
(ext) =>
!(ext.startsWith("feat ") || ext.startsWith("featuring ")),
!(
ext.startsWith("feat ") ||
ext.startsWith("featuring ") ||
ext.startsWith("ft. ")
),
)
.join(" ");
const extensionWords = jointExtensionWords.split(" ").flat();
@@ -563,7 +567,11 @@ export default class ParserService {
.map((ext) => ext.toLowerCase())
.filter(
(ext) =>
!(ext.startsWith("feat ") || ext.startsWith("featuring ")),
!(
ext.startsWith("feat ") ||
ext.startsWith("featuring ") ||
ext.startsWith("ft. ")
),
)
.join(" ");
const extensionWords = jointExtensionWords.split(" ").flat();