mirror of
https://github.com/Jellify-Music/App.git
synced 2025-12-29 14:59:47 -06:00
Cut queue remap churn, speed lyric lookup, clean cast listener cleanup, and avoid redundant HEADs on downloads
This commit is contained in:
@@ -94,8 +94,6 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
CFE47DDB2EA56B0200EB6067 /* icons */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = icons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -399,10 +397,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n";
|
||||
@@ -416,10 +418,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
|
||||
@@ -583,7 +589,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Jellify/Jellify.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 264;
|
||||
DEVELOPMENT_TEAM = WAH9CZ8BPG;
|
||||
@@ -606,7 +612,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cosmonautical.jellify;
|
||||
PRODUCT_NAME = Jellify;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.cosmonautical.jellify";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.cosmonautical.jellify";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Jellify-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -706,10 +712,7 @@
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
@@ -796,10 +799,7 @@
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
@@ -928,10 +928,7 @@
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
|
||||
@@ -18,6 +18,27 @@ type DownloadedFileInfo = {
|
||||
size: number
|
||||
}
|
||||
|
||||
const getExtensionFromUrl = (url: string): string | null => {
|
||||
const sanitized = url.split('?')[0]
|
||||
const lastSegment = sanitized.split('/').pop() ?? ''
|
||||
const match = lastSegment.match(/\.([a-zA-Z0-9]+)$/)
|
||||
return match?.[1] ?? null
|
||||
}
|
||||
|
||||
const normalizeExtension = (ext: string | undefined | null) => {
|
||||
if (!ext) return null
|
||||
const clean = ext.toLowerCase()
|
||||
return clean === 'mpeg' ? 'mp3' : clean
|
||||
}
|
||||
|
||||
const extensionFromContentType = (contentType: string | undefined): string | null => {
|
||||
if (!contentType) return null
|
||||
if (!contentType.includes('/')) return null
|
||||
const [, subtypeRaw] = contentType.split('/')
|
||||
const container = subtypeRaw.split(';')[0]
|
||||
return normalizeExtension(container)
|
||||
}
|
||||
|
||||
export type DeleteDownloadsResult = {
|
||||
deletedCount: number
|
||||
freedBytes: number
|
||||
@@ -29,23 +50,30 @@ export async function downloadJellyfinFile(
|
||||
name: string,
|
||||
songName: string,
|
||||
setDownloadProgress: JellifyDownloadProgressState,
|
||||
preferredExtension?: string | null,
|
||||
): Promise<DownloadedFileInfo> {
|
||||
try {
|
||||
// Fetch the file
|
||||
const headRes = await axios.head(url)
|
||||
const contentType = headRes.headers['content-type']
|
||||
const urlExtension = normalizeExtension(getExtensionFromUrl(url))
|
||||
const hintedExtension = normalizeExtension(preferredExtension)
|
||||
|
||||
// Step 2: Get extension from content-type
|
||||
let extension = 'mp3' // default extension
|
||||
if (contentType && contentType.includes('/')) {
|
||||
const parts = contentType.split('/')
|
||||
const container = parts[1].split(';')[0] // handles "audio/m4a; charset=utf-8"
|
||||
if (container !== 'mpeg') {
|
||||
extension = container // don't use mpeg as an extension, use the default extension
|
||||
let extension = urlExtension ?? hintedExtension ?? null
|
||||
|
||||
if (!extension) {
|
||||
try {
|
||||
const headRes = await axios.head(url)
|
||||
const headExtension = extensionFromContentType(headRes.headers['content-type'])
|
||||
if (headExtension) extension = headExtension
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'HEAD request failed when determining download type, using default',
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Build path
|
||||
if (!extension) extension = 'bin' // fallback without assuming a specific codec
|
||||
|
||||
// Build path
|
||||
const fileName = `${name}.${extension}`
|
||||
const downloadDest = `${RNFS.DocumentDirectoryPath}/${fileName}`
|
||||
|
||||
@@ -138,6 +166,7 @@ export const saveAudio = async (
|
||||
track.item.Id as string,
|
||||
track.title as string,
|
||||
setDownloadProgress,
|
||||
track.mediaSourceInfo?.Container,
|
||||
)
|
||||
let downloadedArtworkFile: DownloadedFileInfo | undefined
|
||||
if (track.artwork) {
|
||||
@@ -146,6 +175,7 @@ export const saveAudio = async (
|
||||
track.item.Id as string,
|
||||
track.title as string,
|
||||
setDownloadProgress,
|
||||
undefined,
|
||||
)
|
||||
}
|
||||
track.url = downloadedTrackFile.uri
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useHideRunTimesSetting } from '../../../stores/settings/app'
|
||||
import { queryClient, ONE_HOUR } from '../../../constants/query-client'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media/utils'
|
||||
import MediaInfoQueryKey from '../../../api/queries/media/keys'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
|
||||
export interface TrackProps {
|
||||
track: BaseItemDto
|
||||
@@ -48,6 +49,19 @@ export interface TrackProps {
|
||||
editing?: boolean | undefined
|
||||
}
|
||||
|
||||
const queueItemsCache = new WeakMap<JellifyTrack[], BaseItemDto[]>()
|
||||
|
||||
const getQueueItems = (queue: JellifyTrack[] | undefined): BaseItemDto[] => {
|
||||
if (!queue?.length) return []
|
||||
|
||||
const cached = queueItemsCache.get(queue)
|
||||
if (cached) return cached
|
||||
|
||||
const mapped = queue.map((entry) => entry.item)
|
||||
queueItemsCache.set(queue, mapped)
|
||||
return mapped
|
||||
}
|
||||
|
||||
export default function Track({
|
||||
track,
|
||||
navigation,
|
||||
@@ -99,7 +113,7 @@ export default function Track({
|
||||
|
||||
// Memoize tracklist for queue loading
|
||||
const memoizedTracklist = useMemo(
|
||||
() => tracklist ?? playQueue?.map((track) => track.item) ?? [],
|
||||
() => tracklist ?? getQueueItems(playQueue),
|
||||
[tracklist, playQueue],
|
||||
)
|
||||
|
||||
|
||||
@@ -201,22 +201,36 @@ export default function Lyrics({
|
||||
}
|
||||
}, [lyrics])
|
||||
|
||||
const lyricStartTimes = useMemo(
|
||||
() => parsedLyrics.map((line) => line.startTime),
|
||||
[parsedLyrics],
|
||||
)
|
||||
|
||||
// Track manually selected lyric for immediate feedback
|
||||
const manuallySelectedIndex = useSharedValue(-1)
|
||||
const manualSelectTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// Find current lyric line based on playback position
|
||||
const currentLyricIndex = useMemo(() => {
|
||||
if (!position || parsedLyrics.length === 0) return -1
|
||||
if (position === null || position === undefined || lyricStartTimes.length === 0) return -1
|
||||
|
||||
// Find the last lyric that has started
|
||||
for (let i = parsedLyrics.length - 1; i >= 0; i--) {
|
||||
if (position >= parsedLyrics[i].startTime) {
|
||||
return i
|
||||
// Binary search to find the last startTime <= position
|
||||
let low = 0
|
||||
let high = lyricStartTimes.length - 1
|
||||
let found = -1
|
||||
|
||||
while (low <= high) {
|
||||
const mid = Math.floor((low + high) / 2)
|
||||
if (position >= lyricStartTimes[mid]) {
|
||||
found = mid
|
||||
low = mid + 1
|
||||
} else {
|
||||
high = mid - 1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}, [position, parsedLyrics])
|
||||
|
||||
return found
|
||||
}, [position, lyricStartTimes])
|
||||
|
||||
// Simple auto-scroll that keeps highlighted lyric in center
|
||||
const scrollToCurrentLyric = useCallback(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import usePlayerEngineStore from '../../../stores/player/engine'
|
||||
import { PlayerEngine } from '../../../stores/player/engine'
|
||||
import { MediaPlayerState, useRemoteMediaClient, useStreamPosition } from 'react-native-google-cast'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const useProgress = (UPDATE_INTERVAL: number): Progress => {
|
||||
const { position, duration, buffered } = useProgressRNTP(UPDATE_INTERVAL)
|
||||
@@ -58,16 +58,27 @@ export const usePlaybackState = (): State | undefined => {
|
||||
const isCasting = playerEngineData === PlayerEngine.GOOGLE_CAST
|
||||
const [playbackState, setPlaybackState] = useState<State | undefined>(state)
|
||||
|
||||
useMemo(() => {
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined
|
||||
|
||||
if (client && isCasting) {
|
||||
client.onMediaStatusUpdated((status) => {
|
||||
const handler = (status: { playerState?: MediaPlayerState } | null) => {
|
||||
if (status?.playerState) {
|
||||
setPlaybackState(castToRNTPState(status.playerState))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const maybeUnsubscribe = client.onMediaStatusUpdated(handler)
|
||||
if (typeof maybeUnsubscribe === 'function') {
|
||||
unsubscribe = maybeUnsubscribe
|
||||
}
|
||||
} else {
|
||||
setPlaybackState(state)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (unsubscribe) unsubscribe()
|
||||
}
|
||||
}, [client, isCasting, state])
|
||||
|
||||
return playbackState
|
||||
|
||||
Reference in New Issue
Block a user