Files
App/components/Network/offlineModeUtils.ts
Violet Caulfield f7968532b2 Offline Mode Updates (#287)
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
2025-04-25 07:54:48 -05:00

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)
}