Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ const App = () => {
else global.isSongCaching = settings.isSongCaching
}, [settings.isSongCaching])

React.useEffect(() => {
global.maxCacheSize = settings.maxCacheSize
}, [settings.maxCacheSize])

React.useEffect(() => {
global.enableMaxCacheSize = settings.enableMaxCacheSize
}, [settings.enableMaxCacheSize])

React.useEffect(() => {
const sysLang = localeLang()

Expand Down
5 changes: 4 additions & 1 deletion app/components/settings/OptionInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ThemeContext } from '~/contexts/theme';
import settingStyles from '~/styles/settings';
import size from '~/styles/size';

const OptionInput = ({ title, placeholder, value, onChangeText, isPassword, autoComplete = 'off', inputMode = undefined, isLast = false, secureTextEntry = undefined }) => {
const OptionInput = ({ title, placeholder, value, onChangeText, isPassword, autoComplete = 'off', inputMode = undefined, isLast = false, secureTextEntry = undefined, disable = false }) => {
const theme = React.useContext(ThemeContext)

return (
Expand All @@ -16,6 +16,7 @@ const OptionInput = ({ title, placeholder, value, onChangeText, isPassword, auto
flex: undefined,
maxWidth: '70%',
width: 'min-content',
opacity: disable ? 0.5 : 1,
}]}>{title}</Text>
<TextInput
style={{
Expand All @@ -29,7 +30,9 @@ const OptionInput = ({ title, placeholder, value, onChangeText, isPassword, auto
web: { outline: 'none' }
}),
overflow: 'hidden',
opacity: disable ? 0.5 : 1,
}}
editable={!disable}
multiline={false}
placeholder={placeholder}
placeholderTextColor={theme.secondaryText}
Expand Down
14 changes: 7 additions & 7 deletions app/components/settings/OptionText.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { View, TextInput, Platform } from 'react-native';
import React from 'react'
import { View, TextInput, Platform } from 'react-native'

import { ThemeContext } from '~/contexts/theme';
import settingStyles from '~/styles/settings';
import size from '~/styles/size';
import { ThemeContext } from '~/contexts/theme'
import settingStyles from '~/styles/settings'
import size from '~/styles/size'

const OptionInput = ({ placeholder, value, onChangeText, isPassword, autoComplete = 'off', inputMode = undefined, isLast = false, secureTextEntry = undefined }) => {
const OptionText = ({ placeholder, value, onChangeText, isPassword, autoComplete = 'off', inputMode = undefined, isLast = false, secureTextEntry = undefined }) => {
const theme = React.useContext(ThemeContext)

return (
Expand Down Expand Up @@ -46,4 +46,4 @@ const OptionInput = ({ placeholder, value, onChangeText, isPassword, autoComplet
)
}

export default OptionInput;
export default OptionText
2 changes: 2 additions & 0 deletions app/contexts/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export const defaultSettings = {
isSongCaching: false,
cacheNextSong: 5,
showCache: true,
enableMaxCacheSize: false,
maxCacheSize: 5,
// Player settings
streamFormat: 'raw',
maxBitRate: 0,
Expand Down
8 changes: 7 additions & 1 deletion app/i18next/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,17 @@
"Show cached songs": "Show cached songs",
"Cache next song": "Cache next song",
"Cache next song description": "Auto download upcoming songs (default: 5)",
"Clear cache": "Clear cache",
"Clear API cache": "Clear API cache",
"Clear song cache": "Clear song cache",
"Clear cache alert message": "Are you sure you want to clear the cache?",
"Cache stats": "Cache stats",
"No cache": "No cache"
"No cache": "No cache",
"Cache size": "Cache size",
"Max cache size": "Max cache size",
"Enable max cache size": "Enable max cache size",
"Max cache size GB": "Max cache size (in GB)",
"Max cache size description": "Set a maximum size for the music cache. When this size is reached, the least recently cached music will be removed from the cache."
},
"shares": {
"No shares found": "No shares found",
Expand Down
12 changes: 9 additions & 3 deletions app/i18next/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@
},
"player": {
"Stream format": "Format du flux",
"Stream format description": "Spécifiez le format du flux à lire.",
"Stream format Description": "Spécifiez le format du flux à lire.",
"Max bitrate": "Débit maximal",
"Max bitrate description": "Spécifiez le débit maximal en kilobits par seconde pour le flux à lire. Des débits plus bas consommeront moins de données mais peuvent entraîner une qualité audio inférieure.",
"Max bitrate Description": "Spécifiez le débit maximal en kilobits par seconde pour le flux à lire. Des débits plus bas consommeront moins de données mais peuvent entraîner une qualité audio inférieure.",
"Play seed first": "Lire la référence en premier"
},
"theme": {
Expand All @@ -76,11 +76,17 @@
"Show cached songs": "Afficher les musiques en cache",
"Cache next song": "Musique suivante en cache",
"Cache next song description": "Télécharger automatiquement les titres à venir (par défaut : 5)",
"Clear cache": "Vider le cache",
"Clear API cache": "Vider le cache de l'API",
"Clear song cache": "Vider le cache des musiques",
"Clear cache alert message": "Êtes-vous sûr de vouloir vider le cache ?",
"Cache stats": "Cache stats",
"No cache": "Aucun cache"
"No cache": "Aucun cache",
"Cache size": "Taille du cache",
"Max cache size": "Taille maximale du cache",
"Enable max cache size": "Activer la taille maximale du cache",
"Max cache size GB": "Taille maximale du cache (en Go)",
"Max cache size description": "Définissez une taille maximale pour le cache des musiques. Lorsque cette taille est atteinte, les musiques les moins récemment mis en cache seront supprimées du cache."
},
"shares": {
"No shares found": "Aucun partage trouvé",
Expand Down
42 changes: 28 additions & 14 deletions app/screens/Settings/Cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const CacheSettings = () => {
const settings = React.useContext(SettingsContext)
const setSettings = React.useContext(SetSettingsContext)
const theme = React.useContext(ThemeContext)
const [cacheNextSong, setCacheNextSong] = React.useState(settings.cacheNextSong.toString())
const [statCache, setStatCache] = React.useState([
{ name: 'Loading...', count: '' },
])
Expand All @@ -39,17 +38,6 @@ const CacheSettings = () => {
getStat()
}, [])

React.useEffect(() => {
setCacheNextSong(settings.cacheNextSong.toString())
}, [settings.cacheNextSong])

React.useEffect(() => {
if (cacheNextSong === '') return
const number = parseInt(cacheNextSong)
if (number === settings.cacheNextSong) return
setSettings({ ...settings, cacheNextSong: number })
}, [cacheNextSong])

return (
<ScrollView
style={mainStyles.mainContainer(theme)}
Expand All @@ -71,13 +59,39 @@ const CacheSettings = () => {
/>
<OptionInput
title={t("settings.cache.Cache next song")}
value={cacheNextSong}
onChangeText={(text) => setCacheNextSong(text.replace(/[^0-9]/g, ''))}
value={settings.cacheNextSong.toString()}
onChangeText={(text) => {
const cleanedText = text.replace(/[^0-9]/g, '')
const number = cleanedText === '' ? 0 : parseInt(cleanedText)
setSettings({ ...settings, cacheNextSong: number })
}}
inputMode="numeric"
isLast
/>
</View>
<Text style={settingStyles.description(theme)}>{t('settings.cache.Cache next song description')}</Text>
<Text style={settingStyles.titleContainer(theme)}>{t('settings.cache.Max cache size')}</Text>
<View style={[settingStyles.optionsContainer(theme), { marginBottom: 5 }]}>
<ButtonSwitch
title={t("settings.cache.Enable max cache size")}
value={settings.enableMaxCacheSize}
onPress={() => setSettings({ ...settings, enableMaxCacheSize: !settings.enableMaxCacheSize })}
/>
<OptionInput
title={t("settings.cache.Max cache size GB")}
value={settings.maxCacheSize.toString()}
onChangeText={(text) => {
const cleanedText = text.replace(/[^0-9]/g, '')
const number = cleanedText === '' ? 0 : parseInt(cleanedText)
setSettings({ ...settings, maxCacheSize: number })
}}
inputMode="numeric"
disable={!settings.enableMaxCacheSize}
isLast
/>
</View>
<Text style={settingStyles.description(theme)}>{t('settings.cache.Max cache size description')}</Text>
<Text style={settingStyles.titleContainer(theme)}>{t('settings.cache.Clear cache')}</Text>
<View style={settingStyles.optionsContainer(theme)}>
<ButtonMenu
title={t("settings.cache.Clear API cache")}
Expand Down
35 changes: 32 additions & 3 deletions app/utils/cache.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export const initCacheSong = async () => {
logger.error('initCacheSong', error)
})
global.listCacheSong = await getListCacheSong() || []
global.sizeCacheSong = await FileSystem.getInfoAsync(getPathDir())
.then(info => info.size)
.catch(() => 0)
}

// Cache Settings
Expand All @@ -106,22 +109,48 @@ export const clearSongCache = async () => {
export const getStatCache = async () => {
return [
{
name: 'Cache Api',
name: 'Cache API',
count: await AsyncStorage.getAllKeys()
.then(keys => keys.filter(key => key.startsWith('http')).length)
.catch(() => 0)
},
{
name: 'Cache Songs',
name: 'Cache songs',
count: await getListCacheSong()
.then(files => files.length)
.catch(() => 0)
},
{
name: 'Cache Songs Size',
name: 'Cache songs vize',
count: await FileSystem.getInfoAsync(getPathDir())
.then(info => `${(info.size / (1024 * 1024)) | 0 || 0} MB`)
.catch(() => '0.00')
},
]
}

export const deleteOldCacheSongs = async () => {
const files = await FileSystem.readDirectoryAsync(getPathDir())

console.log('Current cached songs:', files)
files.sort((a, b) => {
return b.modificationTime - a.modificationTime
})

for (let i = 0; i < 5; i++) {
const file = files[i]
console.log('Deleting old cached song:', `${file}`)
await FileSystem.deleteAsync(`${getPathDir()}${file}`)
global.listCacheSong = global.listCacheSong.filter(f => f !== file)
}
// delete part
files.filter(file => file.endsWith('.part')).forEach(async (file) => {
console.log('Deleting part file:', file)
// await FileSystem.deleteAsync(`${getPathDir()}${file}`)
// global.listCacheSong = global.listCacheSong.filter(f => f !== file)
})

global.sizeCacheSong = await FileSystem.getInfoAsync(getPathDir())
.then(info => info.size)
.catch(() => 0)
}
13 changes: 11 additions & 2 deletions app/utils/player.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as FileSystem from 'expo-file-system';
import AsyncStorage from '@react-native-async-storage/async-storage';

import { urlCover, urlStream } from '~/utils/url';
import { isSongCached, getPathSong } from '~/utils/cache';
import { isSongCached, getPathSong, deleteOldCacheSongs } from '~/utils/cache';
import { nextRandomIndex, prevRandomIndex } from '~/utils/tools';
import logger from '~/utils/logger';

Expand Down Expand Up @@ -115,9 +115,13 @@ export const downloadSong = async (urlStream, id) => {
if (global.songsDownloading.indexOf(id) >= 0) return urlStream
const fileUri = getPathSong(id, global.streamFormat)
const partUri = `${fileUri}.part`
global.songsDownloading.push(id)

if (await isSongCached(null, id, global.streamFormat, global.maxBitRate)) return fileUri
if (global.enableMaxCacheSize && global.sizeCacheSong >= global.maxCacheSize * 1024 * 1024 * 1024) {
logger.info('downloadSong', 'Max cache size reached, skipping download')
return urlStream
}
global.songsDownloading.push(id)
try {
const res = await FileSystem.downloadAsync(urlStream, partUri)
const contentType = getHeader(res?.headers, 'content-type')
Expand All @@ -136,6 +140,7 @@ export const downloadSong = async (urlStream, id) => {
} else {
await FileSystem.moveAsync({ from: partUri, to: fileUri })
global.listCacheSong.push(`${id}.${global.streamFormat}`)
global.sizeCacheSong += realSize
return fileUri
}
} catch (error) {
Expand All @@ -148,6 +153,10 @@ export const downloadNextSong = async (queue, currentIndex) => {
if (!global.isSongCaching) return
const maxIndex = Math.min(global.cacheNextSong, queue.length)

if (global.enableMaxCacheSize && global.sizeCacheSong >= global.maxCacheSize * 1024 * 1024 * 1024) {
logger.info('downloadNextSong', 'Max cache size reached, deleting old cached songs')
await deleteOldCacheSongs()
}
for (let i = -1; i < maxIndex; i++) {
const index = (currentIndex + queue.length + i) % queue.length
if (!queue[index].isLiveStream && queue[index].id.match(/^[a-zA-Z0-9-]*$/)) {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"web": "cross-env PLATFORM=web expo start --web",
"export:web": "cross-env PLATFORM=web expo export -p web && workbox generateSW workbox-config.js",
"eslint": "eslint app index.js App.js -c eslint.config.mjs",
"postinstall": "patch-package"
"postinstall": "patch-package",
"lang": "node ./scripts/VerifyLang.js"
},
"dependencies": {
"@legendapp/list": "^1.1.4",
Expand Down
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
<title>%WEB_TITLE%</title>
<meta name="description" content="Mobile app for navidrome">
<meta name="description" content="Castafiore is a music player that support Navidrome and Subsonic API.">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Castafiore",
"short_name": "Castafiore",
"description": "Mobile app for navidrome",
"description": "Castafiore is a music player that support Navidrome and Subsonic API.",
"lang": "en",
"display": "standalone",
"orientation": "portrait",
Expand Down
33 changes: 33 additions & 0 deletions scripts/VerifyLang.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// script that check if language keys are translated in all files
const fs = require('fs')
const path = require('path')
const languagesDir = path.join(__dirname, '..', 'app', 'i18next')
const baseLanguageFile = path.join(languagesDir, 'en.json')

const baseLanguage = JSON.parse(fs.readFileSync(baseLanguageFile, 'utf8'))

const checkTranslations = (baseObj, compareObj, parentKey = '') => {
for (const key in baseObj) {
const fullKey = parentKey ? `${parentKey}.${key}` : key
if (typeof baseObj[key] === 'object' && baseObj[key] !== null) {
if (!(key in compareObj)) {
console.log(`Missing key: ${fullKey}`)
} else {
checkTranslations(baseObj[key], compareObj[key], fullKey)
}
} else {
if (!(key in compareObj)) {
console.log(`Missing key: ${fullKey}`)
}
}
}
}

fs.readdirSync(languagesDir).forEach(file => {
if (file.endsWith('.json') && file !== 'en.json') {
const filePath = path.join(languagesDir, file)
const compareLanguage = JSON.parse(fs.readFileSync(filePath, 'utf8'))
console.log(`\nChecking translations for ${file}:`)
checkTranslations(baseLanguage, compareLanguage)
}
})