mirror of
https://github.com/Jellify-Music/App.git
synced 2026-03-05 02:19:14 -06:00
Offline mode backend updates to support seamless queuing of offline tracks when in offline mode Offline tracks will always be queued, streamed tracks will be skipped when without a network connection
240 lines
6.3 KiB
TypeScript
240 lines
6.3 KiB
TypeScript
import { MMKV } from 'react-native-mmkv'
|
|
|
|
import RNFS from 'react-native-fs'
|
|
import { JellifyTrack } from '../../types/JellifyTrack'
|
|
import axios from 'axios'
|
|
import { QueryClient } from '@tanstack/react-query'
|
|
import { JellifyDownload } from '../../types/JellifyDownload'
|
|
import DownloadProgress from '../../types/DownloadProgress'
|
|
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
|
|
|
export async function downloadJellyfinFile(
|
|
url: string,
|
|
name: string,
|
|
songName: string,
|
|
queryClient: QueryClient,
|
|
) {
|
|
try {
|
|
// Fetch the file
|
|
const headRes = await axios.head(url)
|
|
const contentType = headRes.headers['content-type']
|
|
console.log('Content-Type:', contentType)
|
|
|
|
// Step 2: Get extension from content-type
|
|
let extension = 'mp3' // default
|
|
if (contentType && contentType.includes('/')) {
|
|
const parts = contentType.split('/')
|
|
extension = parts[1].split(';')[0] // handles "audio/m4a; charset=utf-8"
|
|
}
|
|
|
|
// Step 3: Build path
|
|
const fileName = `${name}.${extension}`
|
|
const downloadDest = `${RNFS.DocumentDirectoryPath}/${fileName}`
|
|
|
|
queryClient.setQueryData(['downloads'], (prev: DownloadProgress) => ({
|
|
...prev,
|
|
[url]: { progress: 0, name: fileName, songName: songName },
|
|
}))
|
|
|
|
// Step 4: Start download with progress
|
|
const options = {
|
|
fromUrl: url,
|
|
toFile: downloadDest,
|
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
begin: (res: any) => {
|
|
console.log('Download started')
|
|
},
|
|
progress: (data: any) => {
|
|
const percent = +(data.bytesWritten / data.contentLength).toFixed(2)
|
|
|
|
queryClient.setQueryData(['downloads'], (prev: DownloadProgress) => ({
|
|
...prev,
|
|
[url]: { progress: percent, name: fileName, songName: songName },
|
|
}))
|
|
},
|
|
background: true,
|
|
progressDivider: 1,
|
|
}
|
|
|
|
const result = await RNFS.downloadFile(options).promise
|
|
console.log('Download complete:', result)
|
|
|
|
return `file://${downloadDest}`
|
|
} catch (error) {
|
|
console.error('Download failed:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const mmkv = new MMKV({
|
|
id: 'offlineMode',
|
|
encryptionKey: 'offlineMode',
|
|
})
|
|
|
|
const MMKV_OFFLINE_MODE_KEYS = {
|
|
AUDIO_CACHE: 'audioCache',
|
|
AUDIO_CACHE_LIMIT: 'audioCacheLimit',
|
|
}
|
|
|
|
export const getDefaultAudioCacheLimit = () => {
|
|
if (!mmkv.contains(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE_LIMIT)) {
|
|
mmkv.set(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE_LIMIT, 20)
|
|
}
|
|
}
|
|
|
|
getDefaultAudioCacheLimit()
|
|
const AUDIO_CACHE_LIMIT = mmkv.getNumber(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE_LIMIT)
|
|
|
|
export const saveAudio = async (
|
|
track: JellifyTrack,
|
|
queryClient: QueryClient,
|
|
isAutoDownloaded: boolean = true,
|
|
) => {
|
|
if (
|
|
isAutoDownloaded &&
|
|
AUDIO_CACHE_LIMIT &&
|
|
(!Number.isFinite(AUDIO_CACHE_LIMIT) || AUDIO_CACHE_LIMIT <= 0)
|
|
) {
|
|
// If the cache limit is not set or is not a number, or is less than 0, Dont Auto Download
|
|
return
|
|
}
|
|
|
|
const existingRaw = mmkv.getString(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE)
|
|
let existingArray: JellifyDownload[] = []
|
|
try {
|
|
if (existingRaw) {
|
|
existingArray = JSON.parse(existingRaw)
|
|
}
|
|
} catch (error) {
|
|
//Ignore
|
|
}
|
|
|
|
try {
|
|
console.log('Downloading audio', track)
|
|
|
|
const downloadtrack = await downloadJellyfinFile(
|
|
track.url,
|
|
track.item.Id as string,
|
|
track.title as string,
|
|
queryClient,
|
|
)
|
|
const dowloadalbum = await downloadJellyfinFile(
|
|
track.artwork as string,
|
|
track.item.Id as string,
|
|
track.title as string,
|
|
queryClient,
|
|
)
|
|
console.log('downloadtrack', downloadtrack)
|
|
if (downloadtrack) {
|
|
track.url = downloadtrack
|
|
track.artwork = dowloadalbum
|
|
}
|
|
|
|
const index = existingArray.findIndex((t) => t.item.Id === track.item.Id)
|
|
|
|
if (index >= 0) {
|
|
// Replace existing
|
|
existingArray[index] = {
|
|
...track,
|
|
savedAt: new Date().toISOString(),
|
|
isAutoDownloaded,
|
|
path: downloadtrack,
|
|
}
|
|
} else {
|
|
// Add new
|
|
existingArray.push({
|
|
...track,
|
|
savedAt: new Date().toISOString(),
|
|
isAutoDownloaded,
|
|
path: downloadtrack,
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
mmkv.set(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE, JSON.stringify(existingArray))
|
|
}
|
|
|
|
export const deleteAudio = async (trackItem: BaseItemDto) => {
|
|
const downloads = getAudioCache()
|
|
|
|
const download = downloads.filter((download) => download.item.Id === trackItem.Id)
|
|
|
|
if (download.length === 1) {
|
|
RNFS.unlink(`${RNFS.DocumentDirectoryPath}/${download[0].item.Id}`)
|
|
setAudioCache([
|
|
...downloads.slice(0, downloads.indexOf(download[0])),
|
|
...downloads.slice(downloads.indexOf(download[0]) + 1, downloads.length - 1),
|
|
])
|
|
}
|
|
}
|
|
|
|
const setAudioCache = (downloads: JellifyDownload[]) => {
|
|
mmkv.set(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE, JSON.stringify(downloads))
|
|
}
|
|
|
|
export const getAudioCache = (): JellifyDownload[] => {
|
|
const existingRaw = mmkv.getString(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE)
|
|
let existingArray: JellifyDownload[] = []
|
|
try {
|
|
if (existingRaw) {
|
|
existingArray = JSON.parse(existingRaw)
|
|
}
|
|
} catch (error) {
|
|
//Ignore
|
|
}
|
|
return existingArray
|
|
}
|
|
|
|
export const deleteAudioCache = async () => {
|
|
mmkv.delete(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE)
|
|
}
|
|
|
|
export const purneAudioCache = async () => {
|
|
const existingRaw = mmkv.getString(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE)
|
|
if (!existingRaw) return
|
|
|
|
let existingArray: JellifyDownload[] = []
|
|
|
|
try {
|
|
existingArray = JSON.parse(existingRaw)
|
|
} catch (e) {
|
|
return
|
|
}
|
|
|
|
const autoDownloads = existingArray
|
|
.filter((item) => item.isAutoDownloaded)
|
|
.sort((a, b) => new Date(a.savedAt).getTime() - new Date(b.savedAt).getTime()) // oldest first
|
|
|
|
const excess = autoDownloads.length - (AUDIO_CACHE_LIMIT ?? 20)
|
|
if (excess <= 0) return
|
|
|
|
// Remove the oldest `excess` files
|
|
const itemsToDelete = autoDownloads.slice(0, excess)
|
|
for (const item of itemsToDelete) {
|
|
// Delete audio file
|
|
if (item.url && (await RNFS.exists(item.url))) {
|
|
await RNFS.unlink(item.url).catch(() => {})
|
|
}
|
|
|
|
// Delete artwork
|
|
if (item.artwork && (await RNFS.exists(item.artwork))) {
|
|
await RNFS.unlink(item.artwork).catch(() => {})
|
|
}
|
|
|
|
// Remove from the existingArray
|
|
existingArray = existingArray.filter((i) => i.item.Id !== item.item.Id)
|
|
}
|
|
|
|
mmkv.set(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE, JSON.stringify(existingArray))
|
|
}
|
|
|
|
export const setAudioCacheLimit = (limit: number) => {
|
|
mmkv.set(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE_LIMIT, limit)
|
|
}
|
|
|
|
export const getAudioCacheLimit = () => {
|
|
return mmkv.getNumber(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE_LIMIT)
|
|
}
|