diff --git a/.gitmodules b/.gitmodules index 3fd143a6f..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "MusicFreePlugins"] - path = MusicFreePlugins - url = https://github.com/maotoumao/MusicFreePlugins.git diff --git a/MusicFreePlugins b/MusicFreePlugins deleted file mode 160000 index cef575149..000000000 --- a/MusicFreePlugins +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cef57514986130b76938a106ca867165654dc206 diff --git a/__tests__/musicfree/mfexample.js b/__tests__/musicfree/mfexample.js new file mode 100644 index 000000000..51e6ecaec --- /dev/null +++ b/__tests__/musicfree/mfexample.js @@ -0,0 +1,542 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +const axios_1 = require('axios'); +const cheerio_1 = require('cheerio'); +const CryptoJS = require('crypto-js'); +const dayjs = require('dayjs'); +const pageSize = 20; +const headers = { + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', +}; +function nonce(e = 10) { + let n = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + r = ''; + for (let i = 0; i < e; i++) + r += n.charAt(Math.floor(Math.random() * n.length)); + return r; +} +function getNormalizedParams(parameters) { + const sortedKeys = []; + const normalizedParameters = []; + for (let e in parameters) { + sortedKeys.push(_encode(e)); + } + sortedKeys.sort(); + for (let idx = 0; idx < sortedKeys.length; idx++) { + const e = sortedKeys[idx]; + var n, + r, + i = _decode(e), + a = parameters[i]; + for (a.sort(), n = 0; n < a.length; n++) + (r = _encode(a[n])), normalizedParameters.push(e + '=' + r); + } + return normalizedParameters.join('&'); +} +function _encode(e) { + return e + ? encodeURIComponent(e) + .replace(/[!'()]/g, escape) + .replace(/\*/g, '%2A') + : ''; +} +function _decode(e) { + return e ? decodeURIComponent(e) : ''; +} +function u(e) { + (this._parameters = {}), this._loadParameters(e || {}); +} +u.prototype = { + _loadParameters: function (e) { + e instanceof Array + ? this._loadParametersFromArray(e) + : 'object' == typeof e && this._loadParametersFromObject(e); + }, + _loadParametersFromArray: function (e) { + var n; + for (n = 0; n < e.length; n++) this._loadParametersFromObject(e[n]); + }, + _loadParametersFromObject: function (e) { + var n; + for (n in e) + if (e.hasOwnProperty(n)) { + var r = this._getStringFromParameter(e[n]); + this._loadParameterValue(n, r); + } + }, + _loadParameterValue: function (e, n) { + var r; + if (n instanceof Array) { + for (r = 0; r < n.length; r++) { + var i = this._getStringFromParameter(n[r]); + this._addParameter(e, i); + } + 0 == n.length && this._addParameter(e, ''); + } else this._addParameter(e, n); + }, + _getStringFromParameter: function (e) { + var n = e || ''; + try { + ('number' == typeof e || 'boolean' == typeof e) && (n = e.toString()); + } catch (e) {} + return n; + }, + _addParameter: function (e, n) { + this._parameters[e] || (this._parameters[e] = []), + this._parameters[e].push(n); + }, + get: function () { + return this._parameters; + }, +}; +function getSignature( + method, + urlPath, + params, + secret = 'f3ac5b086f3eab260520d8e3049561e6', +) { + urlPath = urlPath.split('?')[0]; + urlPath = urlPath.startsWith('http') + ? urlPath + : 'https://api.audiomack.com/v1' + urlPath; + const r = new u(params).get(); + const httpMethod = method.toUpperCase(); + const normdParams = getNormalizedParams(r); + const l = + _encode(httpMethod) + '&' + _encode(urlPath) + '&' + _encode(normdParams); + const hash = CryptoJS.HmacSHA1(l, secret + '&').toString(CryptoJS.enc.Base64); + return hash; +} +function formatMusicItem(raw) { + return { + id: raw.id, + artwork: raw.image || raw.image_base, + duration: +raw.duration, + title: raw.title, + artist: raw.artist, + album: raw.album, + url_slug: raw.url_slug, + }; +} +function formatAlbumItem(raw) { + var _a, _b; + return { + artist: raw.artist, + artwork: raw.image || raw.image_base, + id: raw.id, + date: dayjs.unix(+raw.released).format('YYYY-MM-DD'), + title: raw.title, + _musicList: + (_b = + (_a = raw === null || raw === void 0 ? void 0 : raw.tracks) === null || + _a === void 0 + ? void 0 + : _a.map) === null || _b === void 0 + ? void 0 + : _b.call(_a, it => ({ + id: it.song_id || it.id, + artwork: raw.image || raw.image_base, + duration: +it.duration, + title: it.title, + artist: it.artist, + album: raw.title, + })), + }; +} +function formatMusicSheetItem(raw) { + var _a, _b, _c, _d, _e, _f; + return { + worksNum: raw.track_count, + id: raw.id, + title: raw.title, + artist: (_a = raw.artist) === null || _a === void 0 ? void 0 : _a.name, + artwork: raw.image || raw.image_base, + artistItem: { + id: (_b = raw.artist) === null || _b === void 0 ? void 0 : _b.id, + avatar: + ((_c = raw.artist) === null || _c === void 0 ? void 0 : _c.image) || + ((_d = raw.artist) === null || _d === void 0 ? void 0 : _d.image_base), + name: (_e = raw.artist) === null || _e === void 0 ? void 0 : _e.name, + url_slug: + (_f = raw.artist) === null || _f === void 0 ? void 0 : _f.url_slug, + }, + createAt: dayjs.unix(+raw.created).format('YYYY-MM-DD'), + url_slug: raw.url_slug, + }; +} +async function searchBase(query, page, show) { + const params = { + limit: pageSize, + oauth_consumer_key: 'audiomack-js', + oauth_nonce: nonce(32), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: Math.round(Date.now() / 1e3), + oauth_version: '1.0', + page: page, + q: query, + show: show, + sort: 'popular', + }; + const oauth_signature = getSignature('GET', '/search', params); + const results = ( + await axios_1.default.get('https://api.audiomack.com/v1/search', { + headers, + params: Object.assign(Object.assign({}, params), { oauth_signature }), + }) + ).data.results; + return results; +} +async function searchMusic(query, page) { + const results = await searchBase(query, page, 'songs'); + return { + isEnd: results.length < pageSize, + data: results.map(formatMusicItem), + }; +} +async function searchAlbum(query, page) { + const results = await searchBase(query, page, 'albums'); + return { + isEnd: results.length < pageSize, + data: results.map(formatAlbumItem), + }; +} +async function searchMusicSheet(query, page) { + const results = await searchBase(query, page, 'playlists'); + return { + isEnd: results.length < pageSize, + data: results.map(formatMusicSheetItem), + }; +} +async function searchArtist(query, page) { + const results = await searchBase(query, page, 'artists'); + return { + isEnd: results.length < pageSize, + data: results.map(raw => ({ + name: raw.name, + id: raw.id, + avatar: raw.image || raw.image_base, + url_slug: raw.url_slug, + })), + }; +} +let dataUrlBase; +async function getDataUrlBase() { + if (dataUrlBase) { + return dataUrlBase; + } + const rawHtml = (await axios_1.default.get('https://audiomack.com/')).data; + const $ = (0, cheerio_1.load)(rawHtml); + const script = $('script#__NEXT_DATA__').text(); + const jsonObj = JSON.parse(script); + if (jsonObj.buildId) { + dataUrlBase = `https://audiomack.com/_next/data/${jsonObj.buildId}`; + } + return dataUrlBase; +} +async function getArtistWorks(artistItem, page, type) { + if (type === 'music') { + const params = { + artist_id: artistItem.id, + limit: pageSize, + oauth_consumer_key: 'audiomack-js', + oauth_nonce: nonce(32), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: Math.round(Date.now() / 1e3), + oauth_version: '1.0', + page: page, + sort: 'rank', + type: 'songs', + }; + const oauth_signature = getSignature( + 'GET', + '/search_artist_content', + params, + ); + const results = ( + await axios_1.default.get( + 'https://api.audiomack.com/v1/search_artist_content', + { + headers, + params: Object.assign(Object.assign({}, params), { oauth_signature }), + }, + ) + ).data.results; + return { + isEnd: results.length < pageSize, + data: results.map(formatMusicItem), + }; + } else if (type === 'album') { + const params = { + artist_id: artistItem.id, + limit: pageSize, + oauth_consumer_key: 'audiomack-js', + oauth_nonce: nonce(32), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: Math.round(Date.now() / 1e3), + oauth_version: '1.0', + page: page, + sort: 'rank', + type: 'albums', + }; + const oauth_signature = getSignature( + 'GET', + '/search_artist_content', + params, + ); + const results = ( + await axios_1.default.get( + 'https://api.audiomack.com/v1/search_artist_content', + { + headers, + params: Object.assign(Object.assign({}, params), { oauth_signature }), + }, + ) + ).data.results; + return { + isEnd: results.length < pageSize, + data: results.map(formatAlbumItem), + }; + } +} +async function getMusicSheetInfo(sheet, page) { + const _dataUrlBase = await getDataUrlBase(); + const res = ( + await axios_1.default.get( + `${_dataUrlBase}/${sheet.artistItem.url_slug}/playlist/${sheet.url_slug}.json`, + { + params: { + page_slug: sheet.artistItem.url_slug, + playlist_slug: sheet.url_slug, + }, + headers: Object.assign({}, headers), + }, + ) + ).data; + const musicPage = res.pageProps.initialState.musicPage; + const targetKey = Object.keys(musicPage).find(it => + it.startsWith('musicMusicPage'), + ); + const tracks = musicPage[targetKey].results.tracks; + return { + isEnd: true, + musicList: tracks.map(formatMusicItem), + }; +} +async function getMediaSource(musicItem, quality) { + if (quality !== 'standard') { + return; + } + const params = { + environment: 'desktop-web', + hq: true, + oauth_consumer_key: 'audiomack-js', + oauth_nonce: nonce(32), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: Math.round(Date.now() / 1e3), + oauth_version: '1.0', + section: '/search', + }; + const oauth_signature = getSignature( + 'GET', + `/music/play/${musicItem.id}`, + params, + ); + const res = ( + await axios_1.default.get( + `https://api.audiomack.com/v1/music/play/${musicItem.id}`, + { + headers: Object.assign(Object.assign({}, headers), { + origin: 'https://audiomack.com', + }), + params: Object.assign(Object.assign({}, params), { oauth_signature }), + }, + ) + ).data; + return { + url: res.signedUrl, + }; +} +async function getAlbumInfo(albumItem) { + return { + musicList: albumItem._musicList.map(it => Object.assign({}, it)), + }; +} +async function getRecommendSheetTags() { + const rawHtml = (await axios_1.default.get('https://audiomack.com/playlists')) + .data; + const $ = (0, cheerio_1.load)(rawHtml); + const script = $('script#__NEXT_DATA__').text(); + const jsonObj = JSON.parse(script); + return { + data: [ + { + data: jsonObj.props.pageProps.categories, + }, + ], + }; +} +async function getRecommendSheetsByTag(tag, page) { + if (!tag.id) { + tag = { id: '34', title: "What's New", url_slug: 'whats-new' }; + } + const params = { + featured: 'yes', + limit: pageSize, + oauth_consumer_key: 'audiomack-js', + oauth_nonce: nonce(32), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: Math.round(Date.now() / 1e3), + oauth_version: '1.0', + page: page, + slug: tag.url_slug, + }; + const oauth_signature = getSignature('GET', '/playlist/categories', params); + const results = ( + await axios_1.default.get( + 'https://api.audiomack.com/v1/playlist/categories', + { + headers, + params: Object.assign(Object.assign({}, params), { oauth_signature }), + }, + ) + ).data.results.playlists; + return { + isEnd: results.length < pageSize, + data: results.map(formatMusicSheetItem), + }; +} +async function getTopLists() { + const genres = [ + { + title: 'All Genres', + url_slug: null, + }, + { + title: 'Afrosounds', + url_slug: 'afrobeats', + }, + { + title: 'Hip-Hop/Rap', + url_slug: 'rap', + }, + { + title: 'Latin', + url_slug: 'latin', + }, + { + title: 'Caribbean', + url_slug: 'caribbean', + }, + { + title: 'Pop', + url_slug: 'pop', + }, + { + title: 'R&B', + url_slug: 'rb', + }, + { + title: 'Gospel', + url_slug: 'gospel', + }, + { + title: 'Electronic', + url_slug: 'electronic', + }, + { + title: 'Rock', + url_slug: 'rock', + }, + { + title: 'Punjabi', + url_slug: 'punjabi', + }, + { + title: 'Country', + url_slug: 'country', + }, + { + title: 'Instrumental', + url_slug: 'instrumental', + }, + { + title: 'Podcast', + url_slug: 'podcast', + }, + ]; + return [ + { + title: 'Trending Songs', + data: genres.map(it => { + var _a; + return Object.assign(Object.assign({}, it), { + type: 'trending', + id: (_a = it.url_slug) !== null && _a !== void 0 ? _a : it.title, + }); + }), + }, + { + title: 'Recently Added Music', + data: genres.map(it => { + var _a; + return Object.assign(Object.assign({}, it), { + type: 'recent', + id: (_a = it.url_slug) !== null && _a !== void 0 ? _a : it.title, + }); + }), + }, + ]; +} +async function getTopListDetail(topListItem, page = 1) { + const type = topListItem.type; + const partialUrl = `/music/${topListItem.url_slug ? `${topListItem.url_slug}/` : ''}${type}/page/${page}`; + const url = `https://api.audiomack.com/v1${partialUrl}`; + const params = { + oauth_consumer_key: 'audiomack-js', + oauth_nonce: nonce(32), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: Math.round(Date.now() / 1e3), + oauth_version: '1.0', + type: 'song', + }; + const oauth_signature = getSignature('GET', partialUrl, params); + const results = ( + await axios_1.default.get(url, { + headers, + params: Object.assign(Object.assign({}, params), { oauth_signature }), + }) + ).data.results; + return { + musicList: results.map(formatMusicItem), + }; +} +module.exports = { + platform: 'Audiomack', + version: '0.0.2', + author: '猫头猫', + primaryKey: ['id', 'url_slug'], + srcUrl: + 'https://gitee.com/maotoumao/MusicFreePlugins/raw/v0.1/dist/audiomack/index.js', + cacheControl: 'no-cache', + supportedSearchType: ['music', 'album', 'sheet', 'artist'], + async search(query, page, type) { + if (type === 'music') { + return await searchMusic(query, page); + } else if (type === 'album') { + return await searchAlbum(query, page); + } else if (type === 'sheet') { + return await searchMusicSheet(query, page); + } else if (type === 'artist') { + return await searchArtist(query, page); + } + }, + getMediaSource, + getAlbumInfo, + getMusicSheetInfo, + getArtistWorks, + getRecommendSheetTags, + getRecommendSheetsByTag, + getTopLists, + getTopListDetail, +}; diff --git a/__tests__/musicfree/musicfree.test.js b/__tests__/musicfree/musicfree.test.js new file mode 100644 index 000000000..8f6d86124 --- /dev/null +++ b/__tests__/musicfree/musicfree.test.js @@ -0,0 +1,12 @@ +import { promises as fs } from 'fs'; +import { loadEvalPlugin } from '../../src/utils/mediafetch/evalsdk'; + +test('eval mfsdk', async () => { + const data = await fs.readFile('__tests__/musicfree/mfexample.js', 'utf8'); + const func = loadEvalPlugin(data); + const search = await func.regexFetch({ url: 'wake' }); + expect(search.songList.length).not.toBe(0); + const song = search.songList[0]; + const resolvedURL = await func.resolveURL(song); + expect(resolvedURL).not.toBe(null); +}); diff --git a/package.json b/package.json index 111e020fd..ad0340abf 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@material/material-color-utilities": "^0.3.0", "@react-native-async-storage/async-storage": "^2.1.0", "@react-native-community/cli": "16.0.2", - "@react-native-community/cli-platform-android": "16.0.2", - "@react-native-community/cli-platform-ios": "16.0.2", + "@react-native-community/cli-platform-android": "15.1.3", + "@react-native-community/cli-platform-ios": "15.1.3", "@react-native-community/netinfo": "^11.4.1", "@react-native-cookies/cookies": "^6.2.1", "@react-native-masked-view/masked-view": "^0.3.2", @@ -43,11 +43,11 @@ "@react-navigation/drawer": "^7.1.1", "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0", - "@revenuecat/purchases-js": "^0.14.0", + "@revenuecat/purchases-js": "^0.15.0", "@sentry/react-native": "^6.4.0", "@sharcoux/slider": "8.0.6", "@shopify/flash-list": "^1.7.2", - "@shopify/react-native-skia": "1.7.5", + "@shopify/react-native-skia": "1.7.6", "axios": "^1.7.9", "base-64": "^1.0.0", "base64-js": "^1.5.1", @@ -61,7 +61,7 @@ "deepmerge": "^4.3.1", "dropbox": "git+https://lovegaoshi@github.com/lovegaoshi/dropbox-sdk-js.git", "event-target-polyfill": "^0.0.4", - "expo": "^52.0.19", + "expo": "^52.0.20", "expo-auth-session": "~6.0.1", "expo-clipboard": "~7.0.0", "expo-crypto": "~14.0.1", @@ -93,7 +93,7 @@ "react-native-blob-jsi-helper": "^0.3.1", "react-native-blob-util": "^0.19.11", "react-native-carplay": "2.4.1-beta.0", - "react-native-device-info": "^14.0.1", + "react-native-device-info": "^14.0.2", "react-native-flashdrag-list": "^0.2.4", "react-native-gesture-handler": "2.21.2", "react-native-get-random-values": "^1.11.0", @@ -101,7 +101,7 @@ "react-native-lyric": "https://lovegaoshi@github.com/lovegaoshi/react-native-lyric.git#commit=6f20e83948c29b0d46833ab9173cd81f99d0ab48", "react-native-pager-view": "6.6.1", "react-native-paper": "^5.12.5", - "react-native-purchases": "^8.4.1", + "react-native-purchases": "^8.4.2", "react-native-qrcode-svg": "^6.3.12", "react-native-reanimated": "3.16.5", "react-native-safe-area-context": "^5.0.0", @@ -138,12 +138,14 @@ "@react-native-community/eslint-config": "^3.2.0", "@tsconfig/react-native": "^3.0.5", "@types/base-64": "^1.0.2", + "@types/crypto-js": "^4.2.2", "@types/d3": "^7.4.3", "@types/he": "^1.2.3", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.13", "@types/md5": "^2.3.5", "@types/node": "^22.10.2", + "@types/qs": "^6.9.17", "@types/react": "~18.3.17", "@types/react-native": "^0.73.0", "@types/react-native-background-timer": "^2.0.2", diff --git a/scripts/fixHTTP.py b/scripts/fixHTTP.py index c376ae105..a15282395 100644 --- a/scripts/fixHTTP.py +++ b/scripts/fixHTTP.py @@ -1,7 +1,7 @@ import os from dev_cleartext import fix_content -if __name__ == '__main__': +if __name__ == '__main__1': mfplugin_dir = './MusicFreePlugins/dist' for i in os.listdir(mfplugin_dir): index_js_path = os.path.join(mfplugin_dir, i, 'index.js') diff --git a/src/components/setting/SkinSearchbar.tsx b/src/components/commonui/SearchBar.tsx similarity index 60% rename from src/components/setting/SkinSearchbar.tsx rename to src/components/commonui/SearchBar.tsx index 5bb001cd2..9ee686640 100644 --- a/src/components/setting/SkinSearchbar.tsx +++ b/src/components/commonui/SearchBar.tsx @@ -2,47 +2,43 @@ import React, { useState } from 'react'; import { Searchbar, ProgressBar } from 'react-native-paper'; import { View, StyleSheet } from 'react-native'; -import { useTranslation } from 'react-i18next'; import { useNoxSetting } from '@stores/useApp'; -import useSnack from '@stores/useSnack'; -import logger from '@utils/Logger'; +import useSnack, { SetSnack } from '@stores/useSnack'; + +interface OnSearchProps { + v: string; + progressEmitter: (v: number) => void; + setSnack: (v: SetSnack) => unknown; +} interface Props { - onSearched: (val: any) => void; + defaultSearchText?: string; + onSearch: (p: OnSearchProps) => Promise; + placeholder?: string; } -const CustomSkinSearch = ({ - onSearched = (vals: any) => console.log(vals), + +export default ({ + onSearch = async v => console.log(v), + defaultSearchText = '', + placeholder, }: Props) => { - const { t } = useTranslation(); const setSnack = useSnack(state => state.setSnack); - const [searchVal, setSearchVal] = useState( - 'https://raw.githubusercontent.com/lovegaoshi/azusa-player-mobile/master/src/components/styles/steria.json', - ); + const [searchVal, setSearchVal] = useState(defaultSearchText); const [searchProgress, progressEmitter] = useState(0); const playerStyle = useNoxSetting(state => state.playerStyle); - const handleSearch = async (val = searchVal) => { + const handleSearch = async (v = searchVal) => { progressEmitter(1); - try { - const res = await fetch(val); - const searchedResult = await res.json(); - onSearched(searchedResult); - } catch (e) { - logger.warn(`[SkinSearchbar] failed to search ${e}`); - setSnack({ - snackMsg: { success: t('CustomSkin.SearchFailMsg') }, - }); - } finally { - progressEmitter(0); - } + await onSearch({ v, progressEmitter, setSnack }); + progressEmitter(0); }; return ( handleSearch(searchVal)} @@ -77,5 +73,3 @@ const styles = StyleSheet.create({ }, progressBar: { backgroundColor: 'rgba(0, 0, 0, 0)' }, }); - -export default CustomSkinSearch; diff --git a/src/components/playlist/BiliSearch/BiliSearchbar.tsx b/src/components/playlist/BiliSearch/BiliSearchbar.tsx index 875444259..9a8a1ebca 100644 --- a/src/components/playlist/BiliSearch/BiliSearchbar.tsx +++ b/src/components/playlist/BiliSearch/BiliSearchbar.tsx @@ -10,7 +10,6 @@ import { useNoxSetting } from '@stores/useApp'; import usePlayback from '@hooks/usePlayback'; import useBiliSearch from '@hooks/useBiliSearch'; import SearchMenu from './SearchMenu'; -import { getMusicFreePlugin } from '@utils/ChromeStorage'; import logger from '@utils/Logger'; import { getIcon } from './Icons'; import AutoComplete from '@components/commonui/AutoComplete'; @@ -44,6 +43,7 @@ export default ({ const playerSetting = useNoxSetting(state => state.playerSetting); const searchOption = useNoxSetting(state => state.searchOption); const searchProgress = useNoxSetting(state => state.searchBarProgress); + const mfsdks = useNoxSetting(state => state.MFsdks); const navigationGlobal = useNavigation(); const externalSearchText = useNoxSetting(state => state.externalSearchText); const setExternalSearchText = useNoxSetting( @@ -66,7 +66,7 @@ export default ({ const pressed = useRef(false); const handleMenuPress = (event: GestureResponderEvent) => { - getMusicFreePlugin().then(v => setShowMusicFree(v.length > 0)); + setShowMusicFree(mfsdks.length > 0); setDialogOpen(true); setMenuCoords({ x: event.nativeEvent.pageX, diff --git a/src/components/playlist/BiliSearch/SearchMenu.tsx b/src/components/playlist/BiliSearch/SearchMenu.tsx index 820b1f6c9..ace618a39 100644 --- a/src/components/playlist/BiliSearch/SearchMenu.tsx +++ b/src/components/playlist/BiliSearch/SearchMenu.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'; import { SearchOptions } from '@enums/Storage'; import useAlert from '@components/dialogs/useAlert'; -import { MUSICFREE } from '@utils/mediafetch/musicfree'; import Icons from './Icons'; import { useNoxSetting } from '@stores/useApp'; import { rgb2Hex } from '@utils/Utils'; @@ -33,7 +32,7 @@ export default ({ const { OneWayAlert } = useAlert(); const playerStyle = useNoxSetting(state => state.playerStyle); const setSearchOption = useNoxSetting(state => state.setSearchOption); - const setDefaultSearch = (defaultSearch: SearchOptions | MUSICFREE) => { + const setDefaultSearch = (defaultSearch: SearchOptions) => { toggleVisible(); setSearchOption(defaultSearch); }; @@ -74,8 +73,8 @@ export default ({ {showMusicFree && ( setDefaultSearch(MUSICFREE.aggregated)} - title={`MusicFree.${MUSICFREE.aggregated}`} + onPress={() => setDefaultSearch(SearchOptions.MUSICFREE)} + title={`MusicFree.${SearchOptions.MUSICFREE}`} /> )} {isAndroid && ( diff --git a/src/components/setting/View.tsx b/src/components/setting/View.tsx index fefed63cb..b4749e3fd 100644 --- a/src/components/setting/View.tsx +++ b/src/components/setting/View.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import GeneralSettings from './GeneralSettings'; import AppearanceSettings from './appearances/View'; import AListSettings from './alist/View'; -import DeveloperSettings from './DeveloperSettings'; +import DeveloperSettings from './developer/View'; import SyncSettings from './SyncSettings'; import { useNoxSetting } from '@stores/useApp'; import SettingListItem from './helpers/SettingListItem'; diff --git a/src/components/setting/appearances/SkinSearchbar.tsx b/src/components/setting/appearances/SkinSearchbar.tsx new file mode 100644 index 000000000..07d86561f --- /dev/null +++ b/src/components/setting/appearances/SkinSearchbar.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useNoxSetting } from '@stores/useApp'; +import logger from '@utils/Logger'; +import SearchBar from '@components/commonui/SearchBar'; +import { getUniqObjects } from '@utils/Utils'; + +interface Props { + getThemeID: (skin: NoxTheme.Style) => string; +} + +export default ({ getThemeID }: Props) => { + const { t } = useTranslation(); + const playerStyles = useNoxSetting(state => state.playerStyles); + const setPlayerStyles = useNoxSetting(state => state.setPlayerStyles); + const loadCustomSkin = (skins: NoxTheme.Style[]) => { + // skins MUST BE an array of objects + if (!Array.isArray(skins)) { + throw new Error('requested skin URL is not an array. aborting.'); + } + const uniqueSkins = getUniqObjects( + skins.filter(skin => skin.metaData).concat(playerStyles), + getThemeID, + ); + setPlayerStyles(uniqueSkins); + }; + + return ( + { + try { + const res = await fetch(v); + const searchedResult = await res.json(); + loadCustomSkin(searchedResult); + } catch (e) { + logger.warn(`[SkinSearchbar] failed to search ${e}`); + setSnack({ + snackMsg: { success: t('CustomSkin.SearchFailMsg') }, + }); + } + }} + placeholder={t('CustomSkin.SearchBarLabel')} + /> + ); +}; diff --git a/src/components/setting/appearances/SkinSettings.tsx b/src/components/setting/appearances/SkinSettings.tsx index f92aba2d9..671adabda 100644 --- a/src/components/setting/appearances/SkinSettings.tsx +++ b/src/components/setting/appearances/SkinSettings.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Image } from 'expo-image'; -import { View, SafeAreaView, StyleSheet, LayoutAnimation } from 'react-native'; +import { View, SafeAreaView, LayoutAnimation } from 'react-native'; import { Text, IconButton, @@ -9,14 +9,16 @@ import { } from 'react-native-paper'; import { FlashList } from '@shopify/flash-list'; -import SkinSearchbar from '../SkinSearchbar'; +import SkinSearchbar from './SkinSearchbar'; import { useNoxSetting } from '@stores/useApp'; import AzusaTheme from '@components/styles/AzusaTheme'; import NoxTheme from '@components/styles/NoxTheme'; import AdaptiveTheme from '@components/styles/AdaptiveTheme'; -import { getUniqObjects, execWhenTrue } from '@utils/Utils'; +import { execWhenTrue } from '@utils/Utils'; import GenericSelectDialog from '../../dialogs/GenericSelectDialog'; import { getStyle } from '@utils/StyleStorage'; +import { ItemSelectStyles as styles } from '@components/style'; + interface DisplayTheme extends NoxTheme.Style { builtin: boolean; } @@ -121,17 +123,17 @@ const SkinItem = ({ ); }; +const getThemeID = (skin: NoxTheme.Style) => + `${skin.metaData.themeName}.${skin.metaData.themeAuthor}`; + const SkinSettings = () => { const [selectSkin, setSelectSkin] = React.useState(); const playerStyle = useNoxSetting(state => state.playerStyle); const playerStyles = useNoxSetting(state => state.playerStyles); const setPlayerStyle = useNoxSetting(state => state.setPlayerStyle); - const setPlayerStyles = useNoxSetting(state => state.setPlayerStyles); const allThemes = BuiltInThemes.concat(playerStyles); - const getThemeID = (skin: NoxTheme.Style) => - `${skin.metaData.themeName}.${skin.metaData.themeAuthor}`; const [checked, setChecked] = React.useState(getThemeID(playerStyle)); - const scrollViewRef = React.useRef | null>(null); + const scrollViewRef = React.useRef>(null); const selectTheme = (theme: NoxTheme.Style) => { setChecked(getThemeID(theme)); @@ -140,19 +142,6 @@ const SkinSettings = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.linear); }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const loadCustomSkin = (skins: any) => { - // skins MUST BE an array of objects - if (!Array.isArray(skins)) { - throw new Error('requested skin URL is not an array. aborting.'); - } - const uniqueSkins = getUniqObjects( - skins.filter(skin => skin.metaData).concat(playerStyles), - getThemeID, - ); - setPlayerStyles(uniqueSkins); - }; - React.useEffect(() => { const currentThemeIndex = allThemes.findIndex( theme => getThemeID(theme) === checked, @@ -178,7 +167,7 @@ const SkinSettings = () => { { backgroundColor: playerStyle.customColors.maskedBackgroundColor }, ]} > - + { listRef={scrollViewRef} /> )} + estimatedItemSize={107} /> { ); }; -const styles = StyleSheet.create({ - safeAreaView: { - flex: 1, - }, - skinItemContainer: { - flexDirection: 'row', - }, - skinItemLeftContainer: { - flexDirection: 'row', - paddingVertical: 5, - flex: 5, - paddingLeft: 5, - }, - skinItemImage: { - width: 72, - height: 72, - borderRadius: 40, - }, - skinItemTextContainer: { - paddingLeft: 5, - }, - lightbulbContainer: { - flexDirection: 'row', - }, - lightbulbIcon: { - marginHorizontal: 0, - marginVertical: 0, - marginLeft: -8, - marginTop: -8, - }, - skinItemRightContainer: { - alignContent: 'flex-end', - }, - deleteButton: { - marginLeft: -3, - }, - skinTitleText: { - maxWidth: '100%', - }, -}); - export default SkinSettings; diff --git a/src/components/setting/DeveloperSettings.tsx b/src/components/setting/developer/DeveloperSettings.tsx similarity index 83% rename from src/components/setting/DeveloperSettings.tsx rename to src/components/setting/developer/DeveloperSettings.tsx index 6f03167d4..45c0e2d9c 100644 --- a/src/components/setting/DeveloperSettings.tsx +++ b/src/components/setting/developer/DeveloperSettings.tsx @@ -3,56 +3,32 @@ import { View, ScrollView, Alert } from 'react-native'; import { List } from 'react-native-paper'; import { useTranslation } from 'react-i18next'; import { useStore } from 'zustand'; -import { createNativeStackNavigator } from '@react-navigation/native-stack'; import * as Sentry from '@sentry/react-native'; // eslint-disable-next-line import/no-unresolved import { APPSTORE } from '@env'; import { useNoxSetting } from '@stores/useApp'; import { logStore, LOGLEVEL } from '@utils/Logger'; -import { RenderSetting } from './helpers/RenderSetting'; -import SettingListItem from './helpers/SettingListItem'; +import { RenderSetting } from '../helpers/RenderSetting'; +import SettingListItem from '../helpers/SettingListItem'; import useVersionCheck from '@hooks/useVersionCheck'; -import { SelectSettingEntry, SettingEntry } from './helpers/SettingEntry'; +import { SelectSettingEntry, SettingEntry } from '../helpers/SettingEntry'; import NoxCache from '@utils/Cache'; import useCleanCache from '@hooks/useCleanCache'; import appStore from '@stores/appStore'; import { saveFadeInterval } from '@utils/ChromeStorage'; -import GroupView from '../background/GroupView'; -import PluginSettings from './plugins/View'; -import showLog from './debug/Log'; -import { showDebugLog } from './debug/DebugConsole'; +import GroupView from '../../background/GroupView'; +import showLog from '../debug/Log'; +import { showDebugLog } from '../debug/DebugConsole'; import { getTakanaDesc, disableTanaka, enableTanaka, } from '@hooks/useTanakaAmazingCommodities'; import { isAndroid } from '@utils/RNUtils'; -import SelectSetting from './helpers/SelectSetting'; -import SelectDialogWrapper, { - SelectDialogChildren, -} from './SelectDialogWrapper'; - -enum Icons { - setlog = 'console', - update = 'update', - showlog = 'bug', - cache = 'floppy', - clearcache = 'delete-sweep', - clearOrphanCache = 'delete-empty', - crossfade = 'shuffle-variant', - fade = 'cosine-wave', - plugins = 'puzzle', - Tanaka = 'emoticon-devil', - ArtworkRes = 'quality-high', -} - -enum VIEW { - HOME = 'Settings', - PLUGINS = 'Plugins', -} - -const Stack = createNativeStackNavigator(); +import SelectSetting from '../helpers/SelectSetting'; +import { SelectDialogChildren } from '../SelectDialogWrapper'; +import { Route, Icons } from './enums'; const FadeOptions = [0, 250, 500, 1000]; const CrossFadeOptions = [0, 2500, 5000, 7500, 12000]; @@ -103,7 +79,7 @@ interface HomeProps extends NoxComponent.StackNavigationProps, SelectDialogChildren {} -const Home = ({ +export const Home = ({ navigation, setCurrentSelectOption, setSelectVisible, @@ -224,7 +200,7 @@ const Home = ({ navigation.navigate(VIEW.PLUGINS)} + onPress={() => navigation.navigate(Route.PLUGINS)} settingCategory="Settings" /> {!APPSTORE && ( @@ -335,35 +311,3 @@ const Home = ({ ); }; - -const HomeWrapper = ({ navigation }: NoxComponent.StackNavigationProps) => { - const playerStyle = useNoxSetting(state => state.playerStyle); - return ( - Home({ ...p, navigation })} - /> - ); -}; - -const DevSettingsView = () => { - return ( - - - - - ); -}; - -export default DevSettingsView; diff --git a/src/components/setting/developer/View.tsx b/src/components/setting/developer/View.tsx new file mode 100644 index 000000000..ed6ed5d6c --- /dev/null +++ b/src/components/setting/developer/View.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; + +import PluginSettings from './plugins/View'; +import { useNoxSetting } from '@stores/useApp'; +import SelectDialogWrapper from '../SelectDialogWrapper'; +import { Route } from './enums'; +import { Home } from './DeveloperSettings'; +import MFSettings from './plugins/musicfree/View'; + +const Stack = createNativeStackNavigator(); + +const HomeWrapper = ({ navigation }: NoxComponent.StackNavigationProps) => { + const playerStyle = useNoxSetting(state => state.playerStyle); + return ( + Home({ ...p, navigation })} + /> + ); +}; + +export default () => { + return ( + + + + + + ); +}; diff --git a/src/components/setting/developer/enums.ts b/src/components/setting/developer/enums.ts new file mode 100644 index 000000000..6efa7cbef --- /dev/null +++ b/src/components/setting/developer/enums.ts @@ -0,0 +1,19 @@ +export enum Route { + HOME = 'Settings', + PLUGINS = 'Plugins', + MUSICFREE = 'MusicFree', +} + +export enum Icons { + setlog = 'console', + update = 'update', + showlog = 'bug', + cache = 'floppy', + clearcache = 'delete-sweep', + clearOrphanCache = 'delete-empty', + crossfade = 'shuffle-variant', + fade = 'cosine-wave', + plugins = 'puzzle', + Tanaka = 'emoticon-devil', + ArtworkRes = 'quality-high', +} diff --git a/src/components/setting/plugins/View.tsx b/src/components/setting/developer/plugins/View.tsx similarity index 94% rename from src/components/setting/plugins/View.tsx rename to src/components/setting/developer/plugins/View.tsx index 83f45a6e3..4e8839d98 100644 --- a/src/components/setting/plugins/View.tsx +++ b/src/components/setting/developer/plugins/View.tsx @@ -3,11 +3,11 @@ import { View, StyleSheet } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useNoxSetting } from '@stores/useApp'; -import SettingListItem from '../helpers/SettingListItem'; +import SettingListItem from '../../helpers/SettingListItem'; import { saveRegextractMapping } from '@utils/ChromeStorage'; import { downloadR128GainDB } from './r128gain/Sync'; import useSnack from '@stores/useSnack'; -import MusicFreeButton from './MusicFreeButton'; +import MusicFreeButton from './musicfree/MusicFreeButton'; const updateFromGithub = async () => { const res = await fetch( diff --git a/src/components/setting/developer/plugins/musicfree/MusicFreeButton.tsx b/src/components/setting/developer/plugins/musicfree/MusicFreeButton.tsx new file mode 100644 index 000000000..85affbece --- /dev/null +++ b/src/components/setting/developer/plugins/musicfree/MusicFreeButton.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Image } from 'expo-image'; +import { StyleSheet } from 'react-native'; + +import SettingListItem from '@components/setting/helpers/SettingListItem'; +import { Route } from '@components/setting/developer/enums'; +import useNavigation from '@hooks/useNavigation'; + +const MusicFreeIcon = () => ( + +); + +export default () => { + const navigation = useNavigation(); + return ( + navigation.navigate2(Route.MUSICFREE)} + settingCategory="PluginSettings" + /> + ); +}; +const style = StyleSheet.create({ + musicFreeIcon: { + width: 50, + height: 50, + marginLeft: 20, + marginTop: 4, + }, +}); diff --git a/src/components/setting/developer/plugins/musicfree/Searchbar.tsx b/src/components/setting/developer/plugins/musicfree/Searchbar.tsx new file mode 100644 index 000000000..64b408f8a --- /dev/null +++ b/src/components/setting/developer/plugins/musicfree/Searchbar.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useNoxSetting } from '@stores/useApp'; +import logger from '@utils/Logger'; +import SearchBar from '@components/commonui/SearchBar'; +import { fetchMFsdk } from '@utils/mfsdk'; + +export default () => { + const { t } = useTranslation(); + const addMFsdks = useNoxSetting(state => state.addMFsdks); + return ( + { + try { + addMFsdks(await fetchMFsdk(v)); + } catch (e) { + logger.warn(`[mfsdk] failed to search ${v}: ${e}`); + setSnack({ + snackMsg: { success: t('MFSDK.SearchFailMsg') }, + }); + } + }} + placeholder={t('MFSDK.url')} + /> + ); +}; diff --git a/src/components/setting/developer/plugins/musicfree/View.tsx b/src/components/setting/developer/plugins/musicfree/View.tsx new file mode 100644 index 000000000..ec6096af7 --- /dev/null +++ b/src/components/setting/developer/plugins/musicfree/View.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { View, SafeAreaView, LayoutAnimation } from 'react-native'; +import { Text, IconButton } from 'react-native-paper'; +import { FlashList } from '@shopify/flash-list'; + +import { ItemSelectStyles as styles } from '@components/style'; +import Searchbar from './Searchbar'; +import { useNoxSetting } from '@stores/useApp'; +import { MFsdk } from '@utils/mediafetch/evalsdk'; +import { fetchMFsdk } from '@utils/mfsdk'; + +interface ItemProps { + sdk: MFsdk; + listRef?: React.RefObject>; +} + +const RenderItem = ({ sdk, listRef }: ItemProps) => { + const playerStyle = useNoxSetting(state => state.playerStyle); + const rmMFsdks = useNoxSetting(state => state.rmMFsdks); + const replaceMFsdks = useNoxSetting(state => state.replaceMFsdks); + + const deleteTheme = async () => { + rmMFsdks([sdk]); + listRef?.current?.prepareForLayoutAnimationRender(); + LayoutAnimation.configureNext(LayoutAnimation.Presets.linear); + }; + + return ( + + + + {`${sdk.platform} v${sdk.version} @${sdk.author}`} + + {sdk.srcUrl} + + + + + fetchMFsdk(sdk.srcUrl).then(replaceMFsdks)} + /> + + + + ); +}; + +const MFSettings = () => { + const MFsdks = useNoxSetting(state => state.MFsdks); + const scrollViewRef = React.useRef>(null); + + return ( + + + ( + + )} + estimatedItemSize={100} + /> + + ); +}; + +export default MFSettings; diff --git a/src/components/setting/plugins/r128gain/Sync.ts b/src/components/setting/developer/plugins/r128gain/Sync.ts similarity index 100% rename from src/components/setting/plugins/r128gain/Sync.ts rename to src/components/setting/developer/plugins/r128gain/Sync.ts diff --git a/src/components/setting/plugins/MusicFreeButton.tsx b/src/components/setting/plugins/MusicFreeButton.tsx deleted file mode 100644 index 1f57005b1..000000000 --- a/src/components/setting/plugins/MusicFreeButton.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Image } from 'expo-image'; - -import GenericCheckDialog from '@components/dialogs/GenericCheckDialog'; -import SettingListItem from '../helpers/SettingListItem'; -import { MUSICFREE } from '@utils/mediafetch/musicfree'; -import { getMusicFreePlugin, setMusicFreePlugin } from '@utils/ChromeStorage'; -import { StyleSheet } from 'react-native'; - -const MusicFreeIcon = () => ( - -); - -export default () => { - const { t } = useTranslation(); - const [visible, setVisible] = React.useState(false); - const [favLists] = React.useState(Object.values(MUSICFREE)); - const [selectedIndices, setSelectedIndices] = React.useState([]); - - const showDialog = () => setVisible(true); - const hideDialog = () => setVisible(false); - - const onClick = () => { - showDialog(); - }; - - const onSubmit = (indices: boolean[]) => { - const selectedMusicFreePlugin = []; - for (const [i, v] of indices.entries()) { - if (v) { - selectedMusicFreePlugin.push(favLists[i]); - } - } - setMusicFreePlugin(selectedMusicFreePlugin); - hideDialog(); - }; - - const init = async () => { - const selectedMusicFreePlugin = await getMusicFreePlugin(); - const checks = Array(favLists.length) - .fill(true) - .map((_, index) => selectedMusicFreePlugin.includes(favLists[index])); - setSelectedIndices(checks); - }; - - React.useEffect(() => { - init(); - }, []); - - return ( - <> - - hideDialog()} - selectedIndices={selectedIndices} - /> - - ); -}; -const style = StyleSheet.create({ - musicFreeIcon: { - width: 50, - height: 50, - marginLeft: 20, - marginTop: 4, - }, -}); diff --git a/src/components/style.ts b/src/components/style.ts index 958bc1cb5..9e0401d48 100644 --- a/src/components/style.ts +++ b/src/components/style.ts @@ -180,3 +180,45 @@ export const styles = StyleSheet.create({ alignMiddle: { justifyContent: 'center' }, alignCenter: { alignItems: 'center' }, }); + +export const ItemSelectStyles = StyleSheet.create({ + safeAreaView: { + flex: 1, + }, + skinItemContainer: { + flexDirection: 'row', + }, + skinItemLeftContainer: { + flexDirection: 'row', + paddingVertical: 5, + flex: 5, + paddingLeft: 5, + }, + skinItemImage: { + width: 72, + height: 72, + borderRadius: 40, + }, + skinItemTextContainer: { + paddingLeft: 5, + }, + lightbulbContainer: { + flexDirection: 'row', + }, + lightbulbIcon: { + marginHorizontal: 0, + marginVertical: 0, + marginLeft: -8, + marginTop: -8, + }, + skinItemRightContainer: { + flexDirection: 'row', + alignContent: 'flex-end', + }, + deleteButton: { + marginLeft: -3, + }, + skinTitleText: { + maxWidth: '100%', + }, +}); diff --git a/src/enums/Storage.ts b/src/enums/Storage.ts index 7fcf25fbf..00180ef55 100644 --- a/src/enums/Storage.ts +++ b/src/enums/Storage.ts @@ -20,10 +20,10 @@ export enum StorageKeys { FADE_INTERVAL = 'fadeInterval', COLORTHEME = 'ColorTheme', REGEXTRACT_MAPPING = 'RegexExtract', - MUSICFREE_PLUGIN = 'MusicFreePlugin', AA_PERMISSION = 'AndroidAutoPermission', TANAKA_AMAZING_COMMODITIES = 'TanakaAmazingCommodities', ALIST_CRED = 'AlistCred', + MFSDK_PATHS = 'MusicFreePaths', YTMTOKEN = 'YTMToken', YTMCOOKIES = 'YTMCookies', @@ -34,4 +34,5 @@ export enum SearchOptions { YOUTUBE = 'youtube', YOUTUBEM = 'yt music', ALIST = 'alist', + MUSICFREE = 'musicfree', } diff --git a/src/hooks/useLyricRN.ts b/src/hooks/useLyricRN.ts index 72acebd5a..826d7992b 100644 --- a/src/hooks/useLyricRN.ts +++ b/src/hooks/useLyricRN.ts @@ -23,7 +23,7 @@ export default (currentSong?: NoxMedia.Song, artist = '') => { }) => { if (resolvedLrc) { const lrcpath = `${song.id}.txt`; - writeTxtFile(lrcpath, [newLrcDetail.lyric ?? lrc], 'lrc/'); + writeTxtFile(lrcpath, [newLrcDetail.lyric ?? lrc], 'lrc'); const lyricDeatail: NoxMedia.LyricDetail = { songId: song.id, lyricKey: resolvedLrc.key, @@ -41,7 +41,7 @@ export default (currentSong?: NoxMedia.Song, artist = '') => { if (lrcDetail === undefined) return; let localLrc: string | undefined = undefined; if (lrcDetail.lyric.endsWith('.txt')) { - localLrc = await readTxtFile(lrcDetail.lyric, 'lrc/'); + localLrc = await readTxtFile(lrcDetail.lyric, 'lrc'); if (localLrc) { logger.debug('[lrc] read local lrc and loading...'); } diff --git a/src/hooks/useNavigation.ts b/src/hooks/useNavigation.ts index 890361536..551a99092 100644 --- a/src/hooks/useNavigation.ts +++ b/src/hooks/useNavigation.ts @@ -22,5 +22,8 @@ export default () => { } }; - return { navigate, getState: navigationGlobal.getState }; + const navigate2 = (route: unknown) => + navigationGlobal.navigate(route as never); + + return { navigate, navigate2, getState: navigationGlobal.getState }; }; diff --git a/src/localization/en/translation.json b/src/localization/en/translation.json index 7e72421b3..2e008116d 100644 --- a/src/localization/en/translation.json +++ b/src/localization/en/translation.json @@ -253,7 +253,7 @@ "RegExpName": "Update RegExp", "RegExpDesc": "Update RegExp rules from GitHub", "MusicFreeName": "MusicFree Plugins", - "MusicFreeDesc": "Enable MusicFree plugins", + "MusicFreeDesc": "Do NOT use, uve been warned.", "MusicFreeCheckTitle": "MusicFree Plugins", "R128GainName": "Update R18Gain", "R128GainDesc": "Update R18Gain rules from GitHub", diff --git a/src/localization/zhcn/translation.json b/src/localization/zhcn/translation.json index 8f5c3a44f..875c7071b 100644 --- a/src/localization/zhcn/translation.json +++ b/src/localization/zhcn/translation.json @@ -267,7 +267,7 @@ "RegExpName": "更新 RegExp", "RegExpDesc": "从 Github 更新 RegExp 规则", "MusicFreeName": "MusicFree 插件", - "MusicFreeDesc": "启用 MusicFree 插件", + "MusicFreeDesc": "不要使用 后果自负", "MusicFreeCheckTitle": "MusicFree 插件", "R128GainName": "更新 R18Gain", "R128GainDesc": "从 GitHub 更新 R18Gain 规则", diff --git a/src/objects/Song.ts b/src/objects/Song.ts index 01f0d4486..2c9f05736 100644 --- a/src/objects/Song.ts +++ b/src/objects/Song.ts @@ -4,7 +4,6 @@ import he from 'he'; import { extractParenthesis } from '../utils/re'; import { reExtractSongName } from '@stores/regexStore'; import { Source } from '@enums/MediaFetch'; -import { MUSICFREE } from '@utils/mediafetch/musicfree'; import { i0hdslbHTTPResolve } from '@utils/Utils'; export const DEFAULT_NULL_URL = 'NULL'; @@ -26,7 +25,7 @@ interface SongProps { duration?: number; album?: string; addedDate?: number; - source?: Source | MUSICFREE; + source?: Source; isLive?: boolean; liveStatus?: boolean; metadataOnLoad?: boolean; diff --git a/src/objects/Storage.ts b/src/objects/Storage.ts index a980b8999..b9e7d96a8 100644 --- a/src/objects/Storage.ts +++ b/src/objects/Storage.ts @@ -22,7 +22,7 @@ export const DefaultSetting: NoxStorage.PlayerSettingDict = { hideCoverInMobile: false, loadPlaylistAsArtist: false, sendBiliHeartbeat: false, - noCookieBiliSearch: false, + noCookieBiliSearch: true, playbackMode: NoxRepeatMode.Shuffle, dataSaver: false, fastBiliSearch: true, @@ -31,17 +31,16 @@ export const DefaultSetting: NoxStorage.PlayerSettingDict = { r128gain: false, prefetchTrack: false, chatGPTResolveSongName: false, - trackCoverArtCard: false, + trackCoverArtCard: true, suggestedSkipLongVideo: true, wavyProgressBar: false, screenAlwaysWake: false, biliEditAPI: false, keepForeground: false, - karaokeLyrics: false, + karaokeLyrics: true, accentColor: false, - memoryEfficiency: false, + memoryEfficiency: true, useSuggestion: false, - enableBili: false, noRepeat: false, audioOffload: true, parseEmbeddedArtwork: false, diff --git a/src/stores/useAPMUI.ts b/src/stores/useAPMUI.ts index 81e5d3b50..e92112e56 100644 --- a/src/stores/useAPMUI.ts +++ b/src/stores/useAPMUI.ts @@ -1,7 +1,6 @@ import { StateCreator } from 'zustand'; import { IntentData } from '@enums/Intent'; -import { MUSICFREE } from '@utils/mediafetch/musicfree'; import { SearchOptions } from '@enums/Storage'; import { saveDefaultSearch } from '@utils/ChromeStorage'; @@ -14,8 +13,8 @@ export interface APMUIStore { intentData?: IntentData; setIntentData: (val?: IntentData) => void; - searchOption: SearchOptions | MUSICFREE; - setSearchOption: (val: SearchOptions | MUSICFREE) => void; + searchOption: SearchOptions; + setSearchOption: (val: SearchOptions) => void; gestureMode: boolean; setGestureMode: (val: boolean) => void; diff --git a/src/stores/useApp.ts b/src/stores/useApp.ts index 34299ece9..50e758909 100644 --- a/src/stores/useApp.ts +++ b/src/stores/useApp.ts @@ -19,12 +19,15 @@ import createBottomTab, { BottomTabStore } from './useBottomTab'; import createAPMUI, { APMUIStore } from './useAPMUI'; import createUI, { UIStore } from './useUI'; import createPlaylists, { PlaylistsStore } from './usePlaylists'; +import createMFsdk, { MFsdkStore } from './useMFsdk'; +import { initMFsdk } from '@utils/mfsdk'; interface NoxSetting extends BottomTabStore, APMUIStore, UIStore, - PlaylistsStore { + PlaylistsStore, + MFsdkStore { crossfadeId: string; setCrossfadeId: (val: string) => void; @@ -71,6 +74,7 @@ export const useNoxSetting = create((set, get, storeApi) => ({ ...createAPMUI(set, get, storeApi), ...createUI(set, get, storeApi), ...createPlaylists(set, get, storeApi), + ...createMFsdk(set, get, storeApi), crossfadeId: '', setCrossfadeId: v => set({ crossfadeId: v }), @@ -162,6 +166,7 @@ export const useNoxSetting = create((set, get, storeApi) => ({ }); const initializedPlayerSetting = val.settings; set({ + MFsdks: await initMFsdk(), currentPlayingId: val.lastPlaylistId[1], currentABRepeat: getABRepeatRaw(val.lastPlaylistId[1]), currentPlayingList: playingList, diff --git a/src/stores/useMFsdk.ts b/src/stores/useMFsdk.ts new file mode 100644 index 000000000..1431d940a --- /dev/null +++ b/src/stores/useMFsdk.ts @@ -0,0 +1,42 @@ +import { StateCreator } from 'zustand'; + +import { MFsdk } from '@utils/mediafetch/evalsdk'; +import { getUniqObjects } from '@utils/Utils'; +import { rmMFsdks, addMFsdks } from '@utils/mfsdk'; + +export interface MFsdkStore { + MFsdks: MFsdk[]; + setMFsdks: (mf: MFsdk[]) => void; + addMFsdks: (mf: MFsdk[]) => void; + rmMFsdks: (mf: MFsdk[]) => void; + replaceMFsdks: (mf: MFsdk[]) => void; +} + +const store: StateCreator = (set, get) => ({ + MFsdks: [], + setMFsdks: mf => set({ MFsdks: mf }), + replaceMFsdks: mf => { + if (mf.length === 0) return; + set(s => ({ + MFsdks: s.MFsdks.map(sdk => mf.find(v => v.srcUrl === sdk.srcUrl) ?? sdk), + })); + }, + addMFsdks: mf => { + if (mf.length === 0) return; + addMFsdks(mf.map(v => v.path)); + set(s => ({ + MFsdks: getUniqObjects([...mf, ...s.MFsdks], sdk => sdk.srcUrl), + })); + }, + rmMFsdks: mf => { + if (mf.length === 0) return; + const { MFsdks } = get(); + const rmUrls = mf.map(v => v.srcUrl); + rmMFsdks(MFsdks.filter(v => rmUrls.includes(v.srcUrl)).map(v => v.path)); + set(s => ({ + MFsdks: s.MFsdks.filter(v => !rmUrls.includes(v.srcUrl)), + })); + }, +}); + +export default store; diff --git a/src/stores/useSlices.ts b/src/stores/useSlices.ts new file mode 100644 index 000000000..bf336c120 --- /dev/null +++ b/src/stores/useSlices.ts @@ -0,0 +1,10 @@ +import { StateCreator } from 'zustand'; + +// zustand store slice template. + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface Slice {} + +const store: StateCreator = set => ({}); + +export default store; diff --git a/src/stores/useSnack.ts b/src/stores/useSnack.ts index 7d1514205..0c421eb21 100644 --- a/src/stores/useSnack.ts +++ b/src/stores/useSnack.ts @@ -2,7 +2,7 @@ import { logger } from '../utils/Logger'; import { create } from 'zustand'; import { timeout } from '../utils/Utils'; -interface SetSnack { +export interface SetSnack { snackMsg: { processing?: string; success: string; fail?: string }; snackDuration?: number; onDismiss?: () => void; diff --git a/src/types/media.d.ts b/src/types/media.d.ts index 40b89a0cf..c5ca091e5 100644 --- a/src/types/media.d.ts +++ b/src/types/media.d.ts @@ -1,11 +1,10 @@ import { SortOptions, PlaylistTypes } from '@enums/Playlist'; import { Source } from '@enums/MediaFetch'; -import { MUSICFREE } from '@utils/mediafetch/musicfree'; import { LrcSource } from '@enums/LyricFetch'; declare global { namespace NoxMedia { - type SongSource = Source | MUSICFREE; + type SongSource = Source; export interface Song { id: string; diff --git a/src/types/request.d.ts b/src/types/request.d.ts index 25c005ec1..61a53fe5a 100644 --- a/src/types/request.d.ts +++ b/src/types/request.d.ts @@ -95,7 +95,7 @@ declare global { songList: NoxMedia.Song[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any refreshToken?: any; - refresh?: (v: Playlist) => Promise; + refresh?: (v: NoxMedia.Playlist) => Promise; } } } diff --git a/src/utils/BiliSearch.ts b/src/utils/BiliSearch.ts index b912eb904..8c8d90de8 100644 --- a/src/utils/BiliSearch.ts +++ b/src/utils/BiliSearch.ts @@ -1,8 +1,6 @@ import biliSearchFetch from '@utils/mediafetch/bilisearch'; import ytbVideoFetch from '@utils/mediafetch/ytbvideo'; import localFetch from '@utils/mediafetch/local'; -import { MUSICFREE, searcher } from '@utils/mediafetch/musicfree'; -import { getMusicFreePlugin } from '@utils/ChromeStorage'; import { SearchOptions } from '@enums/Storage'; import steriatkFetch from './mediafetch/steriatk'; import biliVideoSimilarFetch from './mediafetch/biliVideoSimilar'; @@ -35,6 +33,7 @@ import biliFavColleFetch from './mediafetch/biliFavColle'; import alistFetch from './mediafetch/alist'; import acfunFetch from './mediafetch/acfunvideo'; import { logger } from './Logger'; +import { useNoxSetting } from '@stores/useApp'; /** * assign the proper extractor based on the provided url. uses regex. @@ -47,7 +46,7 @@ interface Props { useBiliTag?: boolean; fastSearch?: boolean; cookiedSearch?: boolean; - defaultSearch?: SearchOptions | MUSICFREE; + defaultSearch?: SearchOptions; genericSearch?: boolean; } @@ -129,12 +128,15 @@ export const searchBiliURLs = async ({ case SearchOptions.YOUTUBEM: results = { songList: await fetchInnerTuneSearch(input) }; break; - case MUSICFREE.aggregated: - results.songList = await searcher[MUSICFREE.aggregated]( - input, - await getMusicFreePlugin(), + case SearchOptions.MUSICFREE: { + const songLists = await Promise.all( + useNoxSetting + .getState() + .MFsdks.map(sdk => sdk.regexFetch({ url: input })), ); + results.songList = songLists.map(v => v.songList).flat(); break; + } default: results = await biliSearchFetch.regexFetch({ url: input, diff --git a/src/utils/Bilibili/biliCookies.ts b/src/utils/Bilibili/biliCookies.ts index fcc86628a..f089f08ae 100644 --- a/src/utils/Bilibili/biliCookies.ts +++ b/src/utils/Bilibili/biliCookies.ts @@ -5,32 +5,15 @@ export enum BILICOOKIES { bilijct = 'bili_jct', } -export const getBiliCookie = async (val = 'bili_jct') => +export const getBiliCookie = async (val = BILICOOKIES.bilijct) => (await CookieManager.get('https://www.bilibili.com'))[val]?.value; -export const getBiliJct = async () => - (await CookieManager.get('https://www.bilibili.com'))['bili_jct']?.value; - -export const BiliCookieHeader = async () => { - const SESSDATA = await getBiliCookie('SESSDATA'); - if (!SESSDATA) { - return; - } - return { - method: 'GET', - headers: { - cookie: `SESSDATA=${SESSDATA}`, - }, - referrer: 'https://www.bilibili.com', - // HACK: setting to omit will use whatever cookie I set above. - credentials: 'omit', - }; -}; +export const getBiliJct = () => getBiliCookie(BILICOOKIES.bilijct); export const cookieHeader = async (): Promise => ({ method: 'GET', headers: { - cookie: `SESSDATA=${await getBiliCookie('SESSDATA')}`, + cookie: `SESSDATA=${await getBiliCookie(BILICOOKIES.SESSDATA)}`, }, referrer: 'https://www.bilibili.com', // HACK: setting to omit will use whatever cookie I set above. diff --git a/src/utils/ChromeStorage.ts b/src/utils/ChromeStorage.ts index aab78365d..97bbc5b8b 100644 --- a/src/utils/ChromeStorage.ts +++ b/src/utils/ChromeStorage.ts @@ -20,16 +20,9 @@ import { NoxRepeatMode } from '@enums/RepeatMode'; import { PlaylistTypes } from '@enums/Playlist'; import { StorageKeys, SearchOptions } from '@enums/Storage'; import { DefaultSetting, OverrideSetting } from '@objects/Storage'; -import { MUSICFREE } from '@utils/mediafetch/musicfree'; import { getAlistCred } from './alist/storage'; import { timeFunction } from './Utils'; -export const setMusicFreePlugin = (val: MUSICFREE[]): Promise => - saveItem(StorageKeys.MUSICFREE_PLUGIN, val); - -export const getMusicFreePlugin = (): Promise => - getItem(StorageKeys.MUSICFREE_PLUGIN, []); - export const getFadeInterval = async () => Number(await getItem(StorageKeys.FADE_INTERVAL)) || 0; @@ -60,7 +53,7 @@ export const saveABMapping = async (val: NoxStorage.ABDict) => export const getDefaultSearch = (): Promise => getItem(StorageKeys.DEFAULT_SEARCH, SearchOptions.BILIBILI); -export const saveDefaultSearch = (val: SearchOptions | MUSICFREE) => +export const saveDefaultSearch = (val: SearchOptions) => saveItem(StorageKeys.DEFAULT_SEARCH, val); export const getCachedMediaMapping = () => diff --git a/src/utils/fs.ts b/src/utils/fs.ts index c2825ebc1..378aa272d 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -10,7 +10,7 @@ export const writeTxtFile = ( ) => { RNFetchBlob.fs .writeStream( - `${fsdirs.DocumentDir}/${subfolder}${filename}`, + `${fsdirs.DocumentDir}/${subfolder}/${filename}`, // encoding, should be one of `base64`, `utf8`, `ascii` 'utf8', // should data append to existing content ? @@ -24,16 +24,15 @@ export const writeTxtFile = ( .catch(console.error); }; -export const readTxtFile = (filename: string, subfolder = '') => { - try { - return RNFetchBlob.fs - .readFile(`${fsdirs.DocumentDir}/${subfolder}${filename}`, 'utf8') - .catch(() => undefined); - } catch (e) { - logger.warn(`[fs] readTxtFile error: ${e}`); - return undefined; - } -}; +export const rmTxtFile = (filename: string, subfolder = '') => + RNFetchBlob.fs + .unlink(`${fsdirs.DocumentDir}/${subfolder}/${filename}`) + .catch(e => logger.warn(`[fs] rmTxtFile error: ${e}`)); + +export const readTxtFile = (filename: string, subfolder = '') => + RNFetchBlob.fs + .readFile(`${fsdirs.DocumentDir}/${subfolder}/${filename}`, 'utf8') + .catch(e => logger.warn(`[fs] readTxtFile error: ${e}`)); export const lsFiles = async ( dirpath = `${fsdirs.DocumentDir}`, diff --git a/src/utils/mediafetch/bilisearch.ts b/src/utils/mediafetch/bilisearch.ts index 13d2de93f..039953655 100644 --- a/src/utils/mediafetch/bilisearch.ts +++ b/src/utils/mediafetch/bilisearch.ts @@ -1,7 +1,7 @@ import { logger } from '../Logger'; import { fetchBiliPaginatedAPI } from './paginatedbili'; import bfetch from '../BiliFetch'; -import { getBiliCookie } from '@utils/Bilibili/biliCookies'; +import { getBiliCookie, BILICOOKIES } from '@utils/Bilibili/biliCookies'; import { timestampToSeconds } from '../Utils'; import SongTS from '@objects/Song'; import { Source } from '@enums/MediaFetch'; @@ -19,7 +19,7 @@ const getCookie = async (cookiedSearch = false) => { cookie = `buvid3=${json.data.b_3};buvid4=${json.data.b_4}`; } if (cookiedSearch) { - return `${cookie};SESSDATA=${await getBiliCookie('SESSDATA')}`; + return `${cookie};SESSDATA=${await getBiliCookie(BILICOOKIES.SESSDATA)}`; } return cookie; }; diff --git a/src/utils/mediafetch/evalsdk.ts b/src/utils/mediafetch/evalsdk.ts new file mode 100644 index 000000000..0641b9ea6 --- /dev/null +++ b/src/utils/mediafetch/evalsdk.ts @@ -0,0 +1,163 @@ +import CryptoJs from 'crypto-js'; +import dayjs from 'dayjs'; +import axios from 'axios'; +import bigInt from 'big-integer'; +import qs from 'qs'; +import * as cheerio from 'cheerio'; +import CookieManager from '@react-native-cookies/cookies'; +import he from 'he'; +import { URL } from 'react-native-url-polyfill'; +import logger from '../Logger'; + +const Qualities = ['super', 'high', 'standard', 'low']; + +export interface MFsdk { + path: string; + platform: string; + version: string; + author: string; + srcUrl: string; + supportedSearchType: string[]; + regexFetch: (v: { url: string }) => Promise; + resolveURL: (v: NoxMedia.Song) => Promise; + /* + search: [AsyncFunction: search], + getMediaSource: [AsyncFunction: getMediaSource]; + --------------- + not implemented and doesnt fit APM's purposes + getAlbumInfo: [AsyncFunction: getAlbumInfo]; + getMusicSheetInfo: [AsyncFunction: getMusicSheetInfo]; + getArtistWorks: [AsyncFunction: getArtistWorks]; + getRecommendSheetTags: [AsyncFunction: getRecommendSheetTags]; + getRecommendSheetsByTag: [AsyncFunction: getRecommendSheetsByTag]; + getTopLists: [AsyncFunction: getTopLists]; + getTopListDetail: [AsyncFunction: getTopListDetail]; + */ +} + +const IMusicToNoxMedia = (val: IMusic.IMusicItem, source: string) => { + return { + // HACK: so NoxMedia.Song can be shoved into getMediaSource + ...val, + bvid: String(val.bvid ?? val.id), + name: val.title, + nameRaw: val.title, + singer: val.artist, + singerId: val.id, + cover: + val.artwork ?? + 'https://i2.hdslb.com/bfs/face/b70f6e62e4582d4fa5d48d86047e64eb57d7504e.jpg', + lyric: val.lrc, + parsedName: val.title, + source, + duration: val.duration ?? 0, + } as NoxMedia.Song; +}; + +const searchWrapper = + (search: (s: string, p: number, t: string) => any, sdk: MFsdk) => + async (v: { url: string }): Promise => { + try { + logger.debug(`[mfsdk][${sdk.platform}] searching ${v.url}`); + const results = await search(v.url, 1, 'music'); + const songList = results.data.map((iMusic: any) => + IMusicToNoxMedia(iMusic, sdk.platform), + ); + return { songList }; + } catch { + logger.debug(`[mfsdk][${sdk.platform}] failed to search.`); + return { songList: [] }; + } + }; + +const resolveURLWrapper = + ( + resolveURL: (v: any, quality: string) => Promise<{ url: string }>, + sdk: MFsdk, + ) => + async (v: NoxMedia.Song, qualities = Qualities) => { + for (const quality of qualities) { + const res = await resolveURL(v, quality); + if (res) { + logger.debug( + `[mfsdk][${sdk.platform}] resolved ${v.name} with quality ${quality}`, + ); + return res; + } + } + throw Error( + `[resolveURL] mfsdk ${sdk.platform} v${sdk.version} failed to resolve.`, + ); + }; + +axios.defaults.timeout = 2000; + +const packages: Record = { + cheerio, + 'crypto-js': CryptoJs, + axios, + dayjs, + 'big-integer': bigInt, + qs, + he, + '@react-native-cookies/cookies': CookieManager, +}; + +const _require = (packageName: string) => { + const pkg = packages[packageName]; + pkg.default = pkg; + return pkg; +}; + +const _consoleBind = function ( + method: 'log' | 'error' | 'info' | 'warn', + ...args: any +) { + const fn = console[method]; + if (fn) { + fn(...args); + } +}; + +const _console = { + log: _consoleBind.bind(null, 'log'), + warn: _consoleBind.bind(null, 'warn'), + info: _consoleBind.bind(null, 'info'), + error: _consoleBind.bind(null, 'error'), +}; + +export const loadEvalPlugin = (plugin: string, path = 'MEMORY'): MFsdk => { + const env = { + getUserVariables: () => ({}), + os: 'android', + }; + + const _module: any = { exports: {} }; + let _instance: any; + // eslint-disable-next-line no-new-func + _instance = Function(` + 'use strict'; + return function(require, __musicfree_require, module, exports, console, env, URL) { + ${plugin} + } + `)()( + _require, + _require, + _module, + _module.exports, + _console, + env, + URL, + ); + if (_module.exports.default) { + _instance = _module.exports.default; + } else { + _instance = _module.exports; + } + return { + ..._instance, + path, + regexFetch: searchWrapper(_instance.search, _instance), + resolveURL: resolveURLWrapper(_instance.getMediaSource, _instance), + }; +}; diff --git a/src/utils/mediafetch/mfsdk.ts b/src/utils/mediafetch/mfsdk.ts deleted file mode 100644 index 5d83b49f5..000000000 --- a/src/utils/mediafetch/mfsdk.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* eslint-disable import/no-unresolved */ -// @ts-ignore -import * as fivesing from '@mfsdk/5sing/index'; -// @ts-ignore -import * as kugou from '@mfsdk/kugou/index'; -// @ts-ignore -import * as qq from '@mfsdk/qq/index'; -// @ts-ignore -import * as kuwo from '@mfsdk/kuwo/index'; -// @ts-ignore -import * as maoerfm from '@mfsdk/maoerfm/index'; -// @ts-ignore -import * as migu from '@mfsdk/migu/index'; -// @ts-ignore -import * as netease from '@mfsdk/netease/index'; -// @ts-ignore -import * as qianqian from '@mfsdk/qianqian/index'; -// @ts-ignore -import * as xmly from '@mfsdk/xmly/index'; -// @ts-ignore -import * as kuiaishou from '@mfsdk/kuaishou/index'; -// @ts-ignore -import * as yinyuetai from '@mfsdk/yinyuetai/index'; -// @ts-ignore -import * as youtube from '@mfsdk/youtube/index'; -// @ts-ignore -import * as audiomack from '@mfsdk/audiomack/index'; - -export default { - fivesing, - kugou, - qq, - kuwo, - maoerfm, - migu, - netease, - qianqian, - xmly, - kuiaishou, - yinyuetai, - youtube, - audiomack, -}; diff --git a/src/utils/mediafetch/musicfree.ts b/src/utils/mediafetch/musicfree.ts deleted file mode 100644 index 17a392343..000000000 --- a/src/utils/mediafetch/musicfree.ts +++ /dev/null @@ -1,204 +0,0 @@ -import mfsdk from '@utils/mediafetch/mfsdk'; - -import { logger } from '../Logger'; - -const { - fivesing, - kugou, - qq, - kuwo, - maoerfm, - migu, - netease, - qianqian, - xmly, - kuiaishou, - yinyuetai, - youtube, - audiomack, -} = mfsdk; - -// This is exactly why users should NOT inject whatever scripts -// into your app. - -export enum MUSICFREE { - fivesing = 'fivesing', - kugou = 'kugou', - qq = 'qq', - kuwo = 'kuwo', - maoerfm = 'maoerfm', - migu = 'migu', - netease = 'netease', - qianqian = 'qianqian', - xmly = 'xmly', - kuaishou = 'kuaishou', - yinyuetai = 'yinyuetai', - youtube = 'mfsdkyoutube', - audiomack = 'audiomack', - aggregated = 'aggregated', -} - -const IMusicToNoxMedia = (val: IMusic.IMusicItem, source: MUSICFREE) => { - return { - // HACK: so NoxMedia.Song can be shoved into getMediaSource - ...val, - id: `${source}-${String(val.id)}`, - bvid: String(val.bvid ?? val.id), - name: val.title, - nameRaw: val.title, - singer: val.artist, - singerId: val.id, - cover: - val.artwork || - 'https://i2.hdslb.com/bfs/face/b70f6e62e4582d4fa5d48d86047e64eb57d7504e.jpg', - lyric: val.lrc, - parsedName: val.title, - source, - duration: val.duration | 0, - } as NoxMedia.Song; -}; - -const genericSearch = async ( - query: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - module: any, - source: MUSICFREE, -): Promise => { - try { - const res = await module.search(query, 1, 'music'); - if (!res) return []; - return res.data.map((val: IMusic.IMusicItem) => - IMusicToNoxMedia(val, source), - ); - } catch (e) { - logger.error(`[mfsdk] ${source} failed to resolve`); - logger.error(e); - } - return []; -}; -const fiveSingSearch = async (query: string) => - genericSearch(query, fivesing, MUSICFREE.fivesing); - -const kugouSearch = async (query: string) => - genericSearch(query, kugou, MUSICFREE.kugou); - -const qqSearch = async (query: string) => - genericSearch(query, qq, MUSICFREE.qq); - -const kuwoSearch = async (query: string) => - genericSearch(query, kuwo, MUSICFREE.kuwo); - -const maoerfmSearch = async (query: string) => - genericSearch(query, maoerfm, MUSICFREE.maoerfm); - -const miguSearch = async (query: string) => - genericSearch(query, migu, MUSICFREE.migu); - -const neteaseSearch = async (query: string) => - genericSearch(query, netease, MUSICFREE.netease); - -const qianqianSearch = async (query: string) => - genericSearch(query, qianqian, MUSICFREE.qianqian); - -const xmlySearch = async (query: string) => - genericSearch(query, xmly, MUSICFREE.xmly); - -const kuaishouSearch = async (query: string) => - genericSearch(query, kuiaishou, MUSICFREE.kuaishou); - -const yinyuetaiSearch = async (query: string) => - genericSearch(query, yinyuetai, MUSICFREE.yinyuetai); - -const youtubeSearch = async (query: string) => - genericSearch(query, youtube, MUSICFREE.youtube); - -const audiomackSearch = async (query: string) => - genericSearch(query, audiomack, MUSICFREE.audiomack); - -const aggregatedSearcher = { - [MUSICFREE.fivesing]: fiveSingSearch, - [MUSICFREE.kugou]: kugouSearch, - [MUSICFREE.qq]: qqSearch, - [MUSICFREE.kuwo]: kuwoSearch, - [MUSICFREE.maoerfm]: maoerfmSearch, - [MUSICFREE.migu]: miguSearch, - [MUSICFREE.netease]: neteaseSearch, - [MUSICFREE.qianqian]: qianqianSearch, - [MUSICFREE.xmly]: xmlySearch, - [MUSICFREE.kuaishou]: kuaishouSearch, - [MUSICFREE.yinyuetai]: yinyuetaiSearch, - [MUSICFREE.youtube]: youtubeSearch, - [MUSICFREE.audiomack]: audiomackSearch, - [MUSICFREE.aggregated]: () => { - throw new Error('Function not implemented.'); - }, -}; - -export const searcher = { - ...aggregatedSearcher, - [MUSICFREE.aggregated]: async (query: string, searchWith?: MUSICFREE[]) => { - const res = await Promise.all( - searchWith - ? searchWith.map(key => aggregatedSearcher[key](query)) - : Object.values(aggregatedSearcher).map(searcher => searcher(query)), - ); - return res.flat(); - }, -}; - -type MFResolve = ( - v: IMusic.IMusicItem, - q: string, -) => Promise<{ url: string } | undefined | null>; - -type Resolver = { - [key in MUSICFREE]: ( - v: NoxMedia.Song, - quality?: string, - ) => Promise<{ url: string } | undefined | null>; -}; - -const resolverWrapper = ( - v: NoxMedia.Song, - resolver: MFResolve, - quality = 'high', -) => - resolver( - { - ...v, - id: v.id.substring((v.source?.length ?? -1) + 1), - } as unknown as IMusic.IMusicItem, - quality, - ); - -export const resolver: Resolver = { - [MUSICFREE.fivesing]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, fivesing.getMediaSource, quality), - [MUSICFREE.kugou]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, kugou.getMediaSource, quality), - [MUSICFREE.qq]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, qq.getMediaSource, quality), - [MUSICFREE.kuwo]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, kuwo.getMediaSource, quality), - [MUSICFREE.maoerfm]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, maoerfm.getMediaSource, quality), - [MUSICFREE.migu]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, migu.getMediaSource, quality), - [MUSICFREE.netease]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, netease.getMediaSource, quality), - [MUSICFREE.qianqian]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, qianqian.getMediaSource, quality), - [MUSICFREE.xmly]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, xmly.getMediaSource, quality), - [MUSICFREE.kuaishou]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, kuiaishou.getMediaSource, quality), - [MUSICFREE.yinyuetai]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, yinyuetai.getMediaSource, quality), - [MUSICFREE.youtube]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, youtube.getMediaSource, quality), - [MUSICFREE.audiomack]: (v: NoxMedia.Song, quality?: string) => - resolverWrapper(v, audiomack.getMediaSource, quality), - [MUSICFREE.aggregated]: () => { - throw new Error('Function not implemented.'); - }, -}; diff --git a/src/utils/mediafetch/resolveURL.ts b/src/utils/mediafetch/resolveURL.ts index f72f9e3af..ff2efd422 100644 --- a/src/utils/mediafetch/resolveURL.ts +++ b/src/utils/mediafetch/resolveURL.ts @@ -5,7 +5,6 @@ import bililiveFetch from './bililive'; import biliBangumiFetch from './biliBangumi'; import localFetch from '@utils/mediafetch/local'; import alistFetch from './alist'; -import { resolver, MUSICFREE } from '@utils/mediafetch/musicfree'; import headRequestFetch from './headRequest'; import { logger } from '../Logger'; import { regexMatchOperations } from '../Utils'; @@ -14,8 +13,7 @@ import bilivideoFetch, { } from './bilivideo'; import acfunFetch from './acfunvideo'; import { NULL_TRACK } from '@objects/Song'; - -const MUSICFREESources: NoxMedia.SongSource[] = Object.values(MUSICFREE); +import { useNoxSetting } from '@stores/useApp'; type RegResolve = NoxUtils.RegexMatchResolve< Promise @@ -72,19 +70,15 @@ export const fetchPlayUrlPromise = async ({ ]); logger.debug(`[resolveURL] ${bvid}, ${cid} }`); - const fallback = () => - fetchBiliUrlPromise({ bvid, cid: String(cid), iOS, noBiliR128Gain }); - - if (song.source && MUSICFREESources.includes(song.source)) { - const vsource = song.source as MUSICFREE; - const result = await resolver[vsource](song); - console.warn(result, song); - if (!result || result.url.length === 0) { - logger.error(JSON.stringify(song)); - throw new Error(`[resolveURL] ${bvid}, ${cid} failed.`); + const fallback = () => { + const mfsdks = useNoxSetting.getState().MFsdks; + for (const mfsdk of mfsdks) { + if (mfsdk.platform === song.source) { + return mfsdk.resolveURL(song); + } } - return result; - } + return fetchBiliUrlPromise({ bvid, cid: String(cid), iOS, noBiliR128Gain }); + }; return regexMatchOperations({ song, diff --git a/src/utils/mediafetch/ytbChannel.ytbi.ts b/src/utils/mediafetch/ytbChannel.ytbi.ts index 0b14a73a3..f47109ed0 100644 --- a/src/utils/mediafetch/ytbChannel.ytbi.ts +++ b/src/utils/mediafetch/ytbChannel.ytbi.ts @@ -56,7 +56,7 @@ const regexFetch = async ({ }; export default { // https://www.youtube.com/c/MioriCelesta - regexSearchMatch: /youtube\.com\/c\/([^&\/]+)/, - regexSearchMatch2: /youtube\.com\/(@[^&\/]+)/, + regexSearchMatch: /youtube\.com\/c\/([^&/]+)/, + regexSearchMatch2: /youtube\.com\/(@[^&/]+)/, regexFetch, }; diff --git a/src/utils/mfsdk.ts b/src/utils/mfsdk.ts new file mode 100644 index 000000000..a966212a4 --- /dev/null +++ b/src/utils/mfsdk.ts @@ -0,0 +1,62 @@ +import { saveItem, getItem } from '@utils/ChromeStorageAPI'; +import { StorageKeys } from '@enums/Storage'; +import { readTxtFile, rmTxtFile, writeTxtFile } from '@utils/fs'; +import logger from './Logger'; +import { loadEvalPlugin, MFsdk } from './mediafetch/evalsdk'; +import bFetch from './BiliFetch'; + +const mfsdkSubFolder = 'mfsdk'; + +const getMFsdk = (): Promise => getItem(StorageKeys.MFSDK_PATHS, []); + +export const rmMFsdks = async (paths: string[]) => { + const mfsdkPaths = await getMFsdk(); + saveItem( + StorageKeys.MFSDK_PATHS, + mfsdkPaths.filter(p => !paths.includes(p)), + ); + paths.forEach(path => rmTxtFile(path, mfsdkSubFolder)); +}; + +export const addMFsdks = async (paths: string[]) => { + const mfsdkPaths = await getMFsdk(); + saveItem(StorageKeys.MFSDK_PATHS, [...new Set([...mfsdkPaths, ...paths])]); +}; + +export const initMFsdk = async () => { + const mfsdkPaths = await getMFsdk(); + const mfsdks = await Promise.all( + mfsdkPaths.map(async p => { + try { + const sdkContent = await readTxtFile(p, mfsdkSubFolder); + return loadEvalPlugin(sdkContent, p); + } catch (e) { + logger.warn(`[mfsdk] failed to load mfsdks from init: ${e}`); + return; + } + }), + ); + return mfsdks.filter(v => v !== undefined); +}; + +export const fetchMFsdk = async (url: string): Promise => { + try { + const res = await bFetch(url); + const text = await res.text(); + try { + const json = JSON.parse(text) as { plugins: { url: string }[] }; + const sdks = await Promise.all(json.plugins.map(p => fetchMFsdk(p.url))); + return sdks.flat(); + } catch { + // do nothing + } + const loadedSDK = loadEvalPlugin(text, url); + const sdkLocalPath = `${loadedSDK.platform}.${loadedSDK.version}.js`; + loadedSDK.path = sdkLocalPath; + writeTxtFile(sdkLocalPath, [text], mfsdkSubFolder); + return [loadedSDK]; + } catch (e) { + logger.warn(`[mfsdk] failed to fetch and parse ${url}: ${e}`); + } + return []; +}; diff --git a/yarn.lock b/yarn.lock index ec003caf3..2da5595ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1783,9 +1783,9 @@ __metadata: languageName: node linkType: hard -"@expo/cli@npm:0.22.6": - version: 0.22.6 - resolution: "@expo/cli@npm:0.22.6" +"@expo/cli@npm:0.22.7": + version: 0.22.7 + resolution: "@expo/cli@npm:0.22.7" dependencies: "@0no-co/graphql.web": "npm:^1.0.8" "@babel/runtime": "npm:^7.20.0" @@ -1860,7 +1860,7 @@ __metadata: ws: "npm:^8.12.1" bin: expo-internal: build/bin/cli - checksum: 10c0/4e840ca0d9dc809ae697353fbe62c273da9cab9b1448fea094b954e41b3bf6aadf8518752cc7b8c990a3cf216e53cc58369f813a3df4a005a3849941b3cd8994 + checksum: 10c0/01af54c85010412bcb5c9114d284a4b6d4d4ef16624d1d9208d22f487e3d3de7532b54f4afad082641b32fe82a4f94d4511fc406beae924fec8005e6cf685f5a languageName: node linkType: hard @@ -2892,6 +2892,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config-android@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-config-android@npm:15.1.3" + dependencies: + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + fast-glob: "npm:^3.3.2" + fast-xml-parser: "npm:^4.4.1" + checksum: 10c0/ac0903c70b6e30592a69b23a2080bf6cd9d32c30ef465310a164d7254227ec35749484d6306a6c547a129f8efdc0a56e15d1adeafbcffa96587a767b1e450bc5 + languageName: node + linkType: hard + "@react-native-community/cli-config-android@npm:16.0.2": version: 16.0.2 resolution: "@react-native-community/cli-config-android@npm:16.0.2" @@ -2904,6 +2916,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config-apple@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-config-apple@npm:15.1.3" + dependencies: + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + checksum: 10c0/57526305ef3767a8f89aee2804e6d4fd80843c3b67db21b0bec288f80bf76147dea334e5bcf55867d9c7b3f87f80ccceb1278fb6e97f3ff11888a31a56c82492 + languageName: node + linkType: hard + "@react-native-community/cli-config-apple@npm:16.0.2": version: 16.0.2 resolution: "@react-native-community/cli-config-apple@npm:16.0.2" @@ -2963,6 +2987,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-android@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-platform-android@npm:15.1.3" + dependencies: + "@react-native-community/cli-config-android": "npm:15.1.3" + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + logkitty: "npm:^0.7.1" + checksum: 10c0/31bdb49b6687fc182f3149cd4a041c106e779dbf3d8ad374a8c7c413d4fc8de99324508350e34b9fbe150c4b0c53dcc839793d96dab376a3e50de648e989cf48 + languageName: node + linkType: hard + "@react-native-community/cli-platform-android@npm:16.0.2": version: 16.0.2 resolution: "@react-native-community/cli-platform-android@npm:16.0.2" @@ -2976,6 +3013,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-apple@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-platform-apple@npm:15.1.3" + dependencies: + "@react-native-community/cli-config-apple": "npm:15.1.3" + "@react-native-community/cli-tools": "npm:15.1.3" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-xml-parser: "npm:^4.4.1" + checksum: 10c0/a0903761a038cb95406226b13e356b62e31efc7bb3d8961a11578f1f2df012b475bec290cbedcecb4e0f61d67a6778cc274242e35c87d39ee160279b4cbd1813 + languageName: node + linkType: hard + "@react-native-community/cli-platform-apple@npm:16.0.2": version: 16.0.2 resolution: "@react-native-community/cli-platform-apple@npm:16.0.2" @@ -2989,6 +3039,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-ios@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-platform-ios@npm:15.1.3" + dependencies: + "@react-native-community/cli-platform-apple": "npm:15.1.3" + checksum: 10c0/47b02d73054d63f75c4a813b605701e765d69372267ba3220753159eb2e3430b8d224a092a4ef453652620ee84644d618994a837ce4c5955e8b423d679e7dbd1 + languageName: node + linkType: hard + "@react-native-community/cli-platform-ios@npm:16.0.2": version: 16.0.2 resolution: "@react-native-community/cli-platform-ios@npm:16.0.2" @@ -3015,6 +3074,25 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-tools@npm:15.1.3": + version: 15.1.3 + resolution: "@react-native-community/cli-tools@npm:15.1.3" + dependencies: + appdirsjs: "npm:^1.2.4" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + mime: "npm:^2.4.1" + open: "npm:^6.2.0" + ora: "npm:^5.4.1" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + shell-quote: "npm:^1.7.3" + sudo-prompt: "npm:^9.0.0" + checksum: 10c0/e458f3a5e97456b6fa8741cd8c04ca384b7657df9f53111daaf132911b00b6b5bf08fad2206c8461d0974b71548296b9da669af76dddf7f3261ac5d527df6bcc + languageName: node + linkType: hard + "@react-native-community/cli-tools@npm:16.0.2": version: 16.0.2 resolution: "@react-native-community/cli-tools@npm:16.0.2" @@ -3633,17 +3711,17 @@ __metadata: languageName: node linkType: hard -"@revenuecat/purchases-js@npm:^0.14.0": - version: 0.14.0 - resolution: "@revenuecat/purchases-js@npm:0.14.0" - checksum: 10c0/da0fdf5f774ead1753660f61267110fda421e4130e164e608039d1700ec76381df584bee3d288a84bcfd4de58e6fbe215ff67a084392589d377e87eeb1088029 +"@revenuecat/purchases-js@npm:^0.15.0": + version: 0.15.0 + resolution: "@revenuecat/purchases-js@npm:0.15.0" + checksum: 10c0/c4d8ec9817da9af6357ee2bb98d69e0648885a1b8913d1e8a6e4fabe8e89f4cdcbd9870f66a986fceaae0066f1089040e1820dfdb657c0e8180d1fd446c71a05 languageName: node linkType: hard -"@revenuecat/purchases-typescript-internal@npm:13.12.1": - version: 13.12.1 - resolution: "@revenuecat/purchases-typescript-internal@npm:13.12.1" - checksum: 10c0/f00f8efb1799cd77763e07b719a39de0916ad188a1aea5669aaa3437fd8b2e7b92ff9776595046f237cd0465bfdb4e4bb7d42d4a1959407a4d087a9ec4320945 +"@revenuecat/purchases-typescript-internal@npm:13.13.0": + version: 13.13.0 + resolution: "@revenuecat/purchases-typescript-internal@npm:13.13.0" + checksum: 10c0/4856e1251899c2a415745d334359becc0066f4cf042f685be93e279b9f6cadf98d9cb1241d88936d267d86afc432e01167480ac95d2cb9cabf654a5db5bbce44 languageName: node linkType: hard @@ -3909,9 +3987,9 @@ __metadata: languageName: node linkType: hard -"@shopify/react-native-skia@npm:1.7.5": - version: 1.7.5 - resolution: "@shopify/react-native-skia@npm:1.7.5" +"@shopify/react-native-skia@npm:1.7.6": + version: 1.7.6 + resolution: "@shopify/react-native-skia@npm:1.7.6" dependencies: canvaskit-wasm: "npm:0.39.1" react-reconciler: "npm:0.27.0" @@ -3926,7 +4004,7 @@ __metadata: optional: true bin: setup-skia-web: ./scripts/setup-canvaskit.js - checksum: 10c0/e6dae73b93947bccb08639d7c1798b4263e853f7f3f66170dc9c63b93a93cb9818d8750c2e7a77cd35afffd4673d0b75c9a2f586895085473280f77a3afbfccd + checksum: 10c0/72fdfe0cfd1d483ad5b67aecbdc66e1a8fab9c9fe38b3c27547d31a59919e9e49aaa104ccbd169202ba3301c368b1d5ea5172bb3a0a79b4f8f0bf147f2174369 languageName: node linkType: hard @@ -4040,6 +4118,13 @@ __metadata: languageName: node linkType: hard +"@types/crypto-js@npm:^4.2.2": + version: 4.2.2 + resolution: "@types/crypto-js@npm:4.2.2" + checksum: 10c0/760a2078f36f2a3a1089ef367b0d13229876adcf4bcd6e8824d00d9e9bfad8118dc7e6a3cc66322b083535e12be3a29044ccdc9603bfb12519ff61551a3322c6 + languageName: node + linkType: hard + "@types/d3-array@npm:*": version: 3.2.1 resolution: "@types/d3-array@npm:3.2.1" @@ -4467,6 +4552,13 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:^6.9.17": + version: 6.9.17 + resolution: "@types/qs@npm:6.9.17" + checksum: 10c0/a183fa0b3464267f8f421e2d66d960815080e8aab12b9aadab60479ba84183b1cdba8f4eff3c06f76675a8e42fe6a3b1313ea76c74f2885c3e25d32499c17d1b + languageName: node + linkType: hard + "@types/react-native-background-timer@npm:^2.0.2": version: 2.0.2 resolution: "@types/react-native-background-timer@npm:2.0.2" @@ -5526,8 +5618,8 @@ __metadata: "@material/material-color-utilities": "npm:^0.3.0" "@react-native-async-storage/async-storage": "npm:^2.1.0" "@react-native-community/cli": "npm:16.0.2" - "@react-native-community/cli-platform-android": "npm:16.0.2" - "@react-native-community/cli-platform-ios": "npm:16.0.2" + "@react-native-community/cli-platform-android": "npm:15.1.3" + "@react-native-community/cli-platform-ios": "npm:15.1.3" "@react-native-community/eslint-config": "npm:^3.2.0" "@react-native-community/netinfo": "npm:^11.4.1" "@react-native-cookies/cookies": "npm:^6.2.1" @@ -5539,19 +5631,21 @@ __metadata: "@react-navigation/drawer": "npm:^7.1.1" "@react-navigation/native": "npm:^7.0.14" "@react-navigation/native-stack": "npm:^7.2.0" - "@revenuecat/purchases-js": "npm:^0.14.0" + "@revenuecat/purchases-js": "npm:^0.15.0" "@sentry/react-native": "npm:^6.4.0" "@sharcoux/slider": "npm:8.0.6" "@shopify/flash-list": "npm:^1.7.2" - "@shopify/react-native-skia": "npm:1.7.5" + "@shopify/react-native-skia": "npm:1.7.6" "@tsconfig/react-native": "npm:^3.0.5" "@types/base-64": "npm:^1.0.2" + "@types/crypto-js": "npm:^4.2.2" "@types/d3": "npm:^7.4.3" "@types/he": "npm:^1.2.3" "@types/jest": "npm:^29.5.14" "@types/lodash": "npm:^4.17.13" "@types/md5": "npm:^2.3.5" "@types/node": "npm:^22.10.2" + "@types/qs": "npm:^6.9.17" "@types/react": "npm:~18.3.17" "@types/react-native": "npm:^0.73.0" "@types/react-native-background-timer": "npm:^2.0.2" @@ -5589,7 +5683,7 @@ __metadata: eslint-plugin-react: "npm:^7.37.2" eslint-plugin-react-hooks: "npm:^5.1.0" event-target-polyfill: "npm:^0.0.4" - expo: "npm:^52.0.19" + expo: "npm:^52.0.20" expo-auth-session: "npm:~6.0.1" expo-clipboard: "npm:~7.0.0" expo-crypto: "npm:~14.0.1" @@ -5626,7 +5720,7 @@ __metadata: react-native-blob-util: "npm:^0.19.11" react-native-carplay: "npm:2.4.1-beta.0" react-native-clean-project: "npm:^4.0.3" - react-native-device-info: "npm:^14.0.1" + react-native-device-info: "npm:^14.0.2" react-native-dotenv: "npm:^3.4.11" react-native-flashdrag-list: "npm:^0.2.4" react-native-gesture-handler: "npm:2.21.2" @@ -5635,7 +5729,7 @@ __metadata: react-native-lyric: "https://lovegaoshi@github.com/lovegaoshi/react-native-lyric.git#commit=6f20e83948c29b0d46833ab9173cd81f99d0ab48" react-native-pager-view: "npm:6.6.1" react-native-paper: "npm:^5.12.5" - react-native-purchases: "npm:^8.4.1" + react-native-purchases: "npm:^8.4.2" react-native-qrcode-svg: "npm:^6.3.12" react-native-reanimated: "npm:3.16.5" react-native-safe-area-context: "npm:^5.0.0" @@ -8774,15 +8868,15 @@ __metadata: languageName: node linkType: hard -"expo-font@npm:~13.0.1": - version: 13.0.1 - resolution: "expo-font@npm:13.0.1" +"expo-font@npm:~13.0.2": + version: 13.0.2 + resolution: "expo-font@npm:13.0.2" dependencies: fontfaceobserver: "npm:^2.1.0" peerDependencies: expo: "*" react: "*" - checksum: 10c0/f53737b8a44db9778e03b99f0ad5dd218a1692a78f856bee6cc31c0b960372735ffad05bb7e30d54ec6db17a7cfad83f315a6c90f50ae063f84b15b075943255 + checksum: 10c0/d4bf597bc6d03d40cd1fc029080f25c9b0cf1e48669d3e40747540bb3e20a303c2a7a027344df502a556e19d3ec8572db311bfe67e101a5526dcc29b18759bac languageName: node linkType: hard @@ -8890,12 +8984,12 @@ __metadata: languageName: node linkType: hard -"expo@npm:^52.0.19": - version: 52.0.19 - resolution: "expo@npm:52.0.19" +"expo@npm:^52.0.20": + version: 52.0.20 + resolution: "expo@npm:52.0.20" dependencies: "@babel/runtime": "npm:^7.20.0" - "@expo/cli": "npm:0.22.6" + "@expo/cli": "npm:0.22.7" "@expo/config": "npm:~10.0.6" "@expo/config-plugins": "npm:~9.0.12" "@expo/fingerprint": "npm:0.11.4" @@ -8905,7 +8999,7 @@ __metadata: expo-asset: "npm:~11.0.1" expo-constants: "npm:~17.0.3" expo-file-system: "npm:~18.0.6" - expo-font: "npm:~13.0.1" + expo-font: "npm:~13.0.2" expo-keep-awake: "npm:~14.0.1" expo-modules-autolinking: "npm:2.0.4" expo-modules-core: "npm:2.1.2" @@ -8927,7 +9021,7 @@ __metadata: optional: true bin: expo: bin/cli - checksum: 10c0/fd5019a5fefcda9ae0b252d6255eb3ba8931f17677c26298ee22dfa4fff7e48157e814828af4c11d738737ebbbcf5597a225badea392d0c8d07a91657bc933f2 + checksum: 10c0/9352f8a57b43c7e877d648281250f5af7b49cc50b02b4a9e73284e88528b7b939ba56cc199e33ecc209d55b7b39e9966de8724107398fdf93b23903d8a76bd98 languageName: node linkType: hard @@ -14414,12 +14508,12 @@ __metadata: languageName: node linkType: hard -"react-native-device-info@npm:^14.0.1": - version: 14.0.1 - resolution: "react-native-device-info@npm:14.0.1" +"react-native-device-info@npm:^14.0.2": + version: 14.0.2 + resolution: "react-native-device-info@npm:14.0.2" peerDependencies: react-native: "*" - checksum: 10c0/5894d4106dcc43b9bb132171db31462c39fecf1395602b80df6831d6c77d98dd8be9fa974eeb4d0fd657d5d6fde022131530a5c8490ed3b14d385cc6d89de664 + checksum: 10c0/ac1704cfccd563714ebe7f3abf076af2d26caca6a5bd4789edb1ea67b1d7ce2036924663a7a185d4f99b1730de96b7e69e36b363828b19159081109a8dc588c8 languageName: node linkType: hard @@ -14537,15 +14631,15 @@ __metadata: languageName: node linkType: hard -"react-native-purchases@npm:^8.4.1": - version: 8.4.1 - resolution: "react-native-purchases@npm:8.4.1" +"react-native-purchases@npm:^8.4.2": + version: 8.4.2 + resolution: "react-native-purchases@npm:8.4.2" dependencies: - "@revenuecat/purchases-typescript-internal": "npm:13.12.1" + "@revenuecat/purchases-typescript-internal": "npm:13.13.0" peerDependencies: react: ">= 16.6.3" react-native: "*" - checksum: 10c0/6545ca91383644cccc57d598138a9af54d2b2cff6e0c4ff2b0efe38ed1c0da6974993db0d7ac6c5e50d25a23423db70ec068bf2cb1c4cbece50f448329ee42fd + checksum: 10c0/05f21b0a160342e377c8ec02e79b6e3ea48d4f3c9496d8e3fb11d156d3d011525db0e563257e104f1178532b5872fd4cf7ee067d81a45cb971d55b71cfe3af40 languageName: node linkType: hard