diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 9439ba9..63e1afc 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -114,6 +114,8 @@ PODS:
- React-RCTSettings (= 0.68.1)
- React-RCTText (= 0.68.1)
- React-RCTVibration (= 0.68.1)
+ - react-airplay (1.1.2):
+ - React-Core
- React-callinvoker (0.68.1)
- React-Codegen (0.68.1):
- FBReactNativeSpec (= 0.68.1)
@@ -291,8 +293,6 @@ PODS:
- React-jsinspector (0.68.1)
- React-logger (0.68.1):
- glog
- - react-native-airplay-button (1.1.0):
- - React-Core
- react-native-blur (0.8.0):
- React
- react-native-flipper (0.127.0):
@@ -504,6 +504,7 @@ DEPENDENCIES:
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../node_modules/react-native/`)
+ - react-airplay (from `../node_modules/react-airplay`)
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
- React-Codegen (from `build/generated/ios`)
- React-Core (from `../node_modules/react-native/`)
@@ -515,7 +516,6 @@ DEPENDENCIES:
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- - react-native-airplay-button (from `../node_modules/react-native-airplay-button`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-flipper (from `../node_modules/react-native-flipper`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
@@ -591,6 +591,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/Libraries/TypeSafety"
React:
:path: "../node_modules/react-native/"
+ react-airplay:
+ :path: "../node_modules/react-airplay"
React-callinvoker:
:path: "../node_modules/react-native/ReactCommon/callinvoker"
React-Codegen:
@@ -609,8 +611,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
- react-native-airplay-button:
- :path: "../node_modules/react-native-airplay-button"
react-native-blur:
:path: "../node_modules/@react-native-community/blur"
react-native-flipper:
@@ -700,6 +700,7 @@ SPEC CHECKSUMS:
RCTRequired: 00581111c53531e39e3c6346ef0d2c0cf52a5a37
RCTTypeSafety: 07e03ee7800e7dd65cba8e52ad0c2edb06c96604
React: e61f4bf3c573d0c61c56b53dc3eb1d9daf0768a0
+ react-airplay: 19bc646b4cc698c00772f4cb0e45ca8e280d4c6e
React-callinvoker: 047d47230bb6fd66827f8cb0bea4e944ffd1309b
React-Codegen: bb0403cde7374af091530e84e492589485aab480
React-Core: a4a3a8e10d004b08e013c3d0438259dd89a3894c
@@ -709,7 +710,6 @@ SPEC CHECKSUMS:
React-jsiexecutor: 4a4bae5671b064a2248a690cf75957669489d08c
React-jsinspector: 218a2503198ff28a085f8e16622a8d8f507c8019
React-logger: f79dd3cc0f9b44f5611c6c7862badd891a862cf8
- react-native-airplay-button: 90c7ba52402c8e92342003b8a1ff78dfb4357a9e
react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c
react-native-flipper: b9e2e817604af8da0d5a9ba20a8516e780e30f3c
react-native-netinfo: 3671b091c4843fda5e153612866ef4024b8f5d62
diff --git a/package-lock.json b/package-lock.json
index 0abc9b8..9500a8d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,8 +30,8 @@
"i18n-js": "^3.9.2",
"lodash": "^4.17.21",
"react": "^17.0.2",
+ "react-airplay": "^1.1.2",
"react-native": "^0.68.1",
- "react-native-airplay-button": "^1.1.0",
"react-native-collapsible": "^1.6.0",
"react-native-dotenv": "^3.3.1",
"react-native-fast-image": "^8.5.11",
@@ -14241,6 +14241,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-airplay": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/react-airplay/-/react-airplay-1.1.2.tgz",
+ "integrity": "sha512-elsnY0VLF5N0JqY+jdqFd28BmTnzhda0ljV4vTexFcdhrB6uht70J5QUN2E25U4Y0DWlcgprlxAYfWDbUOjTCQ==",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-devtools-core": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.4.tgz",
@@ -14314,15 +14323,6 @@
"react": "17.0.2"
}
},
- "node_modules/react-native-airplay-button": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/react-native-airplay-button/-/react-native-airplay-button-1.1.0.tgz",
- "integrity": "sha512-UifB3XLh7AS9jWSz4Rn5kzFqmuXNa9NiZqn7BUNGjyie/4AO2omyXfeE3RVjO3rEZz7zBcP3/Wgd0QDBcaSt2Q==",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
"node_modules/react-native-codegen": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.0.16.tgz",
@@ -28093,6 +28093,12 @@
"object-assign": "^4.1.1"
}
},
+ "react-airplay": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/react-airplay/-/react-airplay-1.1.2.tgz",
+ "integrity": "sha512-elsnY0VLF5N0JqY+jdqFd28BmTnzhda0ljV4vTexFcdhrB6uht70J5QUN2E25U4Y0DWlcgprlxAYfWDbUOjTCQ==",
+ "requires": {}
+ },
"react-devtools-core": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.4.tgz",
@@ -28732,12 +28738,6 @@
}
}
},
- "react-native-airplay-button": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/react-native-airplay-button/-/react-native-airplay-button-1.1.0.tgz",
- "integrity": "sha512-UifB3XLh7AS9jWSz4Rn5kzFqmuXNa9NiZqn7BUNGjyie/4AO2omyXfeE3RVjO3rEZz7zBcP3/Wgd0QDBcaSt2Q==",
- "requires": {}
- },
"react-native-codegen": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.0.16.tgz",
diff --git a/package.json b/package.json
index ba794b9..1d4a8cd 100644
--- a/package.json
+++ b/package.json
@@ -34,8 +34,8 @@
"i18n-js": "^3.9.2",
"lodash": "^4.17.21",
"react": "^17.0.2",
+ "react-airplay": "^1.1.2",
"react-native": "^0.68.1",
- "react-native-airplay-button": "^1.1.0",
"react-native-collapsible": "^1.6.0",
"react-native-dotenv": "^3.3.1",
"react-native-fast-image": "^8.5.11",
diff --git a/src/assets/icons/airplay-audio.svg b/src/assets/icons/airplay-audio.svg
new file mode 100644
index 0000000..46737ec
--- /dev/null
+++ b/src/assets/icons/airplay-audio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/backward-end.svg b/src/assets/icons/backward-end.svg
new file mode 100644
index 0000000..f9ec689
--- /dev/null
+++ b/src/assets/icons/backward-end.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/forward-end.svg b/src/assets/icons/forward-end.svg
new file mode 100644
index 0000000..d7f1848
--- /dev/null
+++ b/src/assets/icons/forward-end.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 05179bc..cc58a91 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -30,10 +30,6 @@ const ButtonText = styled.Text<{ active?: boolean }>`
color: ${THEME_COLOR};
font-weight: 500;
font-size: 14px;
-
- ${props => props.active && css`
- color: white;
- `}
`;
const Button = React.forwardRef(function Button(props, ref) {
@@ -47,20 +43,19 @@ const Button = React.forwardRef(function Button(props, ref) {
{Icon &&
+ );
+}
+
+export default Divider;
\ No newline at end of file
diff --git a/src/components/DownloadManager.ts b/src/components/DownloadManager.ts
index e700851..8bd7dde 100644
--- a/src/components/DownloadManager.ts
+++ b/src/components/DownloadManager.ts
@@ -67,8 +67,6 @@ function DownloadManager () {
return;
}
- console.log(ids);
-
/**
* Whenever the store is cleared, existing downloads get "lost" because
* the only reference we have is the store. This function checks for
@@ -94,7 +92,7 @@ function DownloadManager () {
dispatch(completeDownload({
id,
location: file.path,
- size: Number.parseInt(file.size),
+ size: file.size,
}));
});
}
diff --git a/src/screens/Music/stacks/components/TrackListView.tsx b/src/screens/Music/stacks/components/TrackListView.tsx
index 3a2b6da..25e7eae 100644
--- a/src/screens/Music/stacks/components/TrackListView.tsx
+++ b/src/screens/Music/stacks/components/TrackListView.tsx
@@ -25,6 +25,7 @@ import { Header, SubHeader } from 'components/Typography';
import { Text } from 'components/Typography';
import CoverImage from 'components/CoverImage';
+import ticksToDuration from 'utility/ticksToDuration';
const styles = StyleSheet.create({
index: {
@@ -170,8 +171,7 @@ const TrackListView: React.FC = ({
]}
numberOfLines={1}
>
- {Math.round(tracks[trackId]?.RunTimeTicks / 10000000 / 60)}
- :{Math.round(tracks[trackId]?.RunTimeTicks / 10000000 % 60).toString().padStart(2, '0')}
+ {ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)}
diff --git a/src/screens/modals/Player/components/Casting.android.tsx b/src/screens/modals/Player/components/Casting.android.tsx
index b923881..31a4fa1 100644
--- a/src/screens/modals/Player/components/Casting.android.tsx
+++ b/src/screens/modals/Player/components/Casting.android.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { Text } from 'react-native';
import styled from 'styled-components/native';
const Button = styled.View`
@@ -7,7 +8,9 @@ const Button = styled.View`
function Casting() {
return (
-
+
);
}
diff --git a/src/screens/modals/Player/components/Casting.ios.tsx b/src/screens/modals/Player/components/Casting.ios.tsx
index 60c10a8..89c22cc 100644
--- a/src/screens/modals/Player/components/Casting.ios.tsx
+++ b/src/screens/modals/Player/components/Casting.ios.tsx
@@ -1,24 +1,57 @@
+import { Text } from 'components/Typography';
import { THEME_COLOR } from 'CONSTANTS';
-import React from 'react';
-import AirPlayButton from 'react-native-airplay-button';
-import styled from 'styled-components/native';
-import { CastingProps } from './Casting';
+import React, { useCallback } from 'react';
+import { showRoutePicker, useAirplayRoutes } from 'react-airplay';
+import { TouchableOpacity } from 'react-native';
+import styled, { css } from 'styled-components/native';
+import AirplayAudioIcon from 'assets/icons/airplay-audio.svg';
+import useDefaultStyles from 'components/Colors';
-const Button = styled.View`
- margin: 20px 40px;
+const Container = styled.View<{ active?: boolean }>`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex: 1 1 auto;
+
+ ${(props) => props.active && css`
+ padding: 8px;
+ margin: -8px 0;
+ border-radius: 8px;
+ `}
`;
-function Casting({ fill }: CastingProps) {
+const Label = styled(Text)<{ active?: boolean }>`
+ margin-left: 8px;
+ opacity: 0.5;
+ font-size: 13px;
+
+ ${(props) => props.active && css`
+ color: ${THEME_COLOR};
+ opacity: 1;
+ `}
+`;
+
+function Casting() {
+ const defaultStyles = useDefaultStyles();
+ const routes = useAirplayRoutes();
+ const handleClick = useCallback(() => showRoutePicker({}), []);
+
return (
- <>
-
- >
+
+
+
);
}
diff --git a/src/screens/modals/Player/components/MediaControls.tsx b/src/screens/modals/Player/components/MediaControls.tsx
index 15afb4d..99e53a8 100644
--- a/src/screens/modals/Player/components/MediaControls.tsx
+++ b/src/screens/modals/Player/components/MediaControls.tsx
@@ -1,19 +1,14 @@
-import React, { useState, useCallback } from 'react';
-import TrackPlayer, { Event, State, usePlaybackState, useTrackPlayerEvents } from 'react-native-track-player';
+import React from 'react';
+import TrackPlayer, { State, usePlaybackState } from 'react-native-track-player';
import { TouchableOpacity, useColorScheme } from 'react-native';
import styled from 'styled-components/native';
import { useHasNextQueue, useHasPreviousQueue } from 'utility/useQueue';
-import ForwardIcon from 'assets/icons/forwards.svg';
-import BackwardIcon from 'assets/icons/backwards.svg';
+import ForwardIcon from 'assets/icons/forward-end.svg';
+import BackwardIcon from 'assets/icons/backward-end.svg';
import PlayIcon from 'assets/icons/play.svg';
import PauseIcon from 'assets/icons/pause.svg';
-import RepeatIcon from 'assets/icons/repeat.svg';
-// import ShuffleIcon from 'assets/icons/shuffle.svg';
-import { THEME_COLOR } from 'CONSTANTS';
-import Casting from './Casting';
const BUTTON_SIZE = 40;
-const BUTTON_SIZE_SMALL = 25;
const pause = TrackPlayer.pause;
const play = TrackPlayer.play;
@@ -22,7 +17,7 @@ const previous = TrackPlayer.skipToPrevious;
const Container = styled.View`
align-items: center;
- margin: 20px 0;
+ margin-top: 40px;
`;
const Buttons = styled.View`
@@ -33,7 +28,8 @@ const Buttons = styled.View`
`;
const Button = styled.View`
- margin: 20px 40px;
+ margin: 0 40px;
+ opacity: 0.75;
`;
export default function MediaControls() {
@@ -51,12 +47,6 @@ export default function MediaControls() {
-
-
-
-
);
}
@@ -81,51 +71,6 @@ export function NextButton({ fill }: { fill: string }) {
);
}
-export function RepeatButton({ fill }: { fill: string}) {
- const [isRepeating, setRepeating] = useState(false);
- const handlePress = useCallback(() => setRepeating(!isRepeating), [isRepeating, setRepeating]);
-
- // The callback that should determine whether we need to repeeat or not
- useTrackPlayerEvents([Event.PlaybackQueueEnded], async () => {
- if (isRepeating) {
- // Skip to the first track
- await TrackPlayer.skip(0);
-
- // Cautiously reset the seek time, as there might only be a single
- // item in queue.
- await TrackPlayer.seekTo(0);
-
- // Then play the item
- await TrackPlayer.play();
- }
- });
-
- return (
-
-
-
- );
-}
-
-// export function ShuffleButton({ fill }: { fill: string}) {
-// const [isShuffling, setShuffling] = useState(false);
-// const handlePress = useCallback(() => setShuffling(!isShuffling), [isShuffling, setShuffling]);
-
-// return (
-//
-//
-//
-// );
-// }
-
export function MainButton({ fill }: { fill: string }) {
const state = usePlaybackState();
diff --git a/src/screens/modals/Player/components/ProgressBar.tsx b/src/screens/modals/Player/components/ProgressBar.tsx
index 0405843..a26060e 100644
--- a/src/screens/modals/Player/components/ProgressBar.tsx
+++ b/src/screens/modals/Player/components/ProgressBar.tsx
@@ -20,7 +20,7 @@ import Reanimated, {
import ReText from 'components/ReText';
const DRAG_HANDLE_SIZE = 20;
-const PADDING_TOP = 14;
+const PADDING_TOP = 12;
const Container = styled.View`
padding-top: ${PADDING_TOP}px;
diff --git a/src/screens/modals/Player/components/Queue.tsx b/src/screens/modals/Player/components/Queue.tsx
index a3dd233..7efe3d5 100644
--- a/src/screens/modals/Player/components/Queue.tsx
+++ b/src/screens/modals/Player/components/Queue.tsx
@@ -1,29 +1,53 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import useQueue from 'utility/useQueue';
import { View, StyleSheet } from 'react-native';
import styled, { css } from 'styled-components/native';
import useCurrentTrack from 'utility/useCurrentTrack';
import TouchableHandler from 'components/TouchableHandler';
-import TrackPlayer from 'react-native-track-player';
+import TrackPlayer, { RepeatMode } from 'react-native-track-player';
import { t } from '@localisation';
import useDefaultStyles from 'components/Colors';
import { Text } from 'components/Typography';
-
+import RepeatIcon from 'assets/icons/repeat.svg';
import Button from 'components/Button';
import { THEME_COLOR } from 'CONSTANTS';
import DownloadIcon from 'components/DownloadIcon';
+import Divider from 'components/Divider';
+import ticksToDuration from 'utility/ticksToDuration';
+
+const ICON_SIZE = 16;
+
+const Container = styled.View`
+ margin-top: 56px;
+`;
+
+const Header = styled.View`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-bottom: 8px;
+`;
+
+const IconButton = styled.TouchableOpacity`
+ padding: 8px;
+ border-radius: 4px;
+`;
+
+const TextHalfOpacity = styled(Text)`
+ opacity: 0.5;
+`;
const QueueItem = styled.View<{ active?: boolean, alreadyPlayed?: boolean, isDark?: boolean }>`
- padding: 10px;
- border-bottom-width: 1px;
+ padding: 8px 0;
flex: 0 0 auto;
flex-direction: row;
align-items: center;
${props => props.active && css`
font-weight: 900;
- padding: 20px 35px;
- margin: 0 -25px;
+ padding: 8px 18px;
+ margin: 0 -18px;
+ border-radius: 8px;
`}
${props => props.alreadyPlayed && css`
@@ -44,18 +68,49 @@ const styles = StyleSheet.create({
export default function Queue() {
const defaultStyles = useDefaultStyles();
const queue = useQueue();
+ const [isRepeating, setIsRepeating] = useState(false);
const { index: currentIndex } = useCurrentTrack();
+
const playTrack = useCallback(async (index: number) => {
await TrackPlayer.skip(index);
await TrackPlayer.play();
}, []);
+
const clearQueue = useCallback(async () => {
await TrackPlayer.reset();
}, []);
+ const toggleLoop = useCallback(() => {
+ setIsRepeating((prev) => {
+ TrackPlayer.setRepeatMode(prev ? RepeatMode.Off : RepeatMode.Queue);
+ return !prev;
+ });
+ }, []);
+
+ // Retrieve the repeat mode and assign it to the state on component mount
+ useEffect(() => {
+ TrackPlayer.getRepeatMode()
+ .then((mode) => {
+ setIsRepeating(mode === RepeatMode.Queue);
+ });
+ }, []);
+
return (
-
- {t('queue')}
+
+
{queue.map((track, i) => (
-
- {track.title}
- {track.artist}
+
+
+ {track.title}
+
+
+ {track.artist}{track.album && ' — ' + track.album}
+
-
-
+
+
+ {ticksToDuration(track.duration || 0)}
+
+
+
+
@@ -80,6 +152,6 @@ export default function Queue() {
-
+
);
}
\ No newline at end of file
diff --git a/src/screens/modals/Player/components/StreamStatus.tsx b/src/screens/modals/Player/components/StreamStatus.tsx
new file mode 100644
index 0000000..54832b3
--- /dev/null
+++ b/src/screens/modals/Player/components/StreamStatus.tsx
@@ -0,0 +1,61 @@
+import React, { useMemo } from 'react';
+import useCurrentTrack from 'utility/useCurrentTrack';
+import CloudIcon from 'assets/icons/cloud.svg';
+import InternalDriveIcon from 'assets/icons/internal-drive.svg';
+import useDefaultStyles from 'components/Colors';
+import { Text } from 'components/Typography';
+import styled from 'styled-components/native';
+import Casting from './Casting';
+
+const ICON_SIZE = 16;
+
+const Container = styled.View`
+ display: flex;
+ justify-content: space-between;
+ flex-direction: row;
+ margin-top: 24px;
+`;
+
+const Group = styled.View`
+ display: flex;
+ flex-direction: row;
+ margin-right: 8px;
+ flex: 1 1 auto;
+`;
+
+const Label = styled(Text)`
+ margin-left: 8px;
+ opacity: 0.5;
+ font-size: 13px;
+`;
+
+function StreamStatus() {
+ const { track } = useCurrentTrack();
+ const defaultStyles = useDefaultStyles();
+
+ const isLocalPlay = useMemo(() => {
+ const url = track?.url;
+ return typeof url === 'string' ? url.startsWith('file://') : false;
+ }, [track?.url]);
+
+ return (
+
+
+ {isLocalPlay ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+ );
+}
+
+export default StreamStatus;
diff --git a/src/screens/modals/Player/index.tsx b/src/screens/modals/Player/index.tsx
index d865056..1983f20 100644
--- a/src/screens/modals/Player/index.tsx
+++ b/src/screens/modals/Player/index.tsx
@@ -8,6 +8,7 @@ import useDefaultStyles from 'components/Colors';
import ConnectionNotice from './components/ConnectionNotice';
import { ScrollView } from 'react-native-gesture-handler';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import StreamStatus from './components/StreamStatus';
const styles = StyleSheet.create({
inner: {
@@ -23,6 +24,7 @@ export default function Player() {
+
diff --git a/src/utility/JellyfinApi.ts b/src/utility/JellyfinApi.ts
index 360188f..27a1e0f 100644
--- a/src/utility/JellyfinApi.ts
+++ b/src/utility/JellyfinApi.ts
@@ -42,6 +42,7 @@ export function generateTrack(track: AlbumTrack, credentials: Credentials): Trac
title: track.Name,
artist: track.Artists.join(', '),
album: track.Album,
+ duration: track.RunTimeTicks,
artwork: track.AlbumId
? getImage(track.AlbumId, credentials)
: getImage(track.Id, credentials),
diff --git a/src/utility/ticksToDuration.ts b/src/utility/ticksToDuration.ts
new file mode 100644
index 0000000..8d2092e
--- /dev/null
+++ b/src/utility/ticksToDuration.ts
@@ -0,0 +1,9 @@
+function ticksToDuration(ticks: number) {
+ const seconds = Math.round(ticks / 10000000);
+ const minutes = Math.round(seconds / 60);
+ const hours = Math.round(minutes / 60);
+
+ return `${hours > 0 ? hours + ':' : ''}${minutes}:${(seconds % 60).toString().padStart(2, '0')}`;
+}
+
+export default ticksToDuration;
\ No newline at end of file
diff --git a/src/utility/usePlayTracks.ts b/src/utility/usePlayTracks.ts
index 0f773d7..d5ca54d 100644
--- a/src/utility/usePlayTracks.ts
+++ b/src/utility/usePlayTracks.ts
@@ -57,7 +57,7 @@ export default function usePlayTracks() {
// Check if a downloaded version exists, and if so rewrite the URL
const download = downloads[trackId];
if (download?.location) {
- generatedTrack.url = download.location;
+ generatedTrack.url = 'file://' + download.location;
}
return generatedTrack;