Compare commits

..

13 Commits

Author SHA1 Message Date
Lei Nelissen
a97611c0ad chore: release v2.3.3 2024-06-15 23:23:15 +02:00
Lei Nelissen
e511f744ad chore: default xcode scheme to debug 2024-05-26 23:53:43 +02:00
Lei Nelissen
a6a306b5be fix: refactor JellyfinApi to be less burdensome to implement
Also, automatically catch errors
2024-05-26 23:53:29 +02:00
Lei Nelissen
881ab95029 fix: double-check albums have dates 2024-05-26 22:20:14 +02:00
Lei Nelissen
968e98d8df fix: react-native-screens android setup 2024-05-26 22:20:05 +02:00
Lei Nelissen
b01470bde8 fix: actually send out /Playing events as session updates.
This should more consistently result in output data in your play back reporting modules.

fixes #218
2024-05-26 18:00:05 +02:00
Lei Nelissen
823f7b59e8 Merge pull request #199 from leinelissen/dependabot/npm_and_yarn/ip-1.1.9
chore(deps): bump ip from 1.1.8 to 1.1.9
2024-05-26 17:07:09 +02:00
Lei Nelissen
16162d8e35 fix: throw errors when requests do not yield 200 OKs 2024-05-26 00:34:57 +02:00
Lei Nelissen
ea817025e1 fix: hermes version in cocoapods 2024-05-26 00:24:04 +02:00
Lei Nelissen
00675bbbd3 fix: do extra checks for album ids in 2024-05-26 00:23:29 +02:00
Lei Nelissen
24b5a47a7c Merge pull request #211 from Krafting/patch-1
Add spaces to privacy-policy.md
2024-04-19 17:27:18 +02:00
Krafting
bb655cb719 Add spaces to privacy-policy.md 2024-04-01 14:19:13 +02:00
dependabot[bot]
e472d043cf chore(deps): bump ip from 1.1.8 to 1.1.9
Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9.
- [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-10 22:09:29 +00:00
33 changed files with 497 additions and 428 deletions

View File

@@ -1,3 +1,37 @@
## [2.3.3](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.3.2...v2.3.3) (2024-06-15)
### Bug Fixes
* actually send out /Playing events as session updates. ([b01470b](https://github.com/leinelissen/jellyfin-audio-player/commit/b01470bde8ea353ea7139c0708ec9cfdaf600fe4)), closes [#218](https://github.com/leinelissen/jellyfin-audio-player/issues/218)
* do extra checks for album ids in ([00675bb](https://github.com/leinelissen/jellyfin-audio-player/commit/00675bbbd3e72e8e710d8aa9b73b491e65153d40))
* double-check albums have dates ([881ab95](https://github.com/leinelissen/jellyfin-audio-player/commit/881ab9502960786dc9685cf3612793fea3c1be4c))
* hermes version in cocoapods ([ea81702](https://github.com/leinelissen/jellyfin-audio-player/commit/ea817025e1bf67fcd3c183c12f4f1f93c3218785))
* react-native-screens android setup ([968e98d](https://github.com/leinelissen/jellyfin-audio-player/commit/968e98d8dffa79ea3165d1209542bd91dd914ef5))
* refactor JellyfinApi to be less burdensome to implement ([a6a306b](https://github.com/leinelissen/jellyfin-audio-player/commit/a6a306b5be6988469449b17ed527f1d365901e6d))
* throw errors when requests do not yield 200 OKs ([16162d8](https://github.com/leinelissen/jellyfin-audio-player/commit/16162d8e3505ea195c8aaf03b82df88405196025))
## [2.3.2](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.3.1...v2.3.2) (2024-03-10)
### Bug Fixes
* build with xcode 15.3 ([845eac7](https://github.com/leinelissen/jellyfin-audio-player/commit/845eac70a0afa189cd76e97f739ad627f648566a))
* remove conflicting app transport properties ([c966276](https://github.com/leinelissen/jellyfin-audio-player/commit/c9662769faec8771b6a70da815ec36e62c8c43a2))
## [2.3.1](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.3.0...v2.3.1) (2024-03-06)
### Bug Fixes
* revert to supporting HTTP-based backends ([f310bb8](https://github.com/leinelissen/jellyfin-audio-player/commit/f310bb82f61f532f9557787d364e9f342166806d)), closes [#205](https://github.com/leinelissen/jellyfin-audio-player/issues/205)
# [2.3.0](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.2.0...v2.3.0) (2024-02-11)

View File

@@ -85,8 +85,8 @@ android {
applicationId "nl.moeilijkedingen.jellyfinaudioplayer"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 27
versionName "2.3.2"
versionCode 29
versionName "2.3.3"
}
signingConfigs {

View File

@@ -5,6 +5,8 @@ import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import android.os.Bundle;
class MainActivity : ReactActivity() {
/**
@@ -19,4 +21,8 @@ class MainActivity : ReactActivity() {
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}
}

View File

@@ -1,10 +1,10 @@
Privacy policy for Fintunes
Fintunes does not collect any personal data. Period. We respect your right to
Fintunes does not collect any personal data. Period. We respect your right to
autonomy and vow to not collect any information without user consent at all.
If you opt-in to crash logging, we will collect analytics data from your device,
every time a crash occurs. This data includes debugging information such as
devices, versions and the specific error. All data is sent to a server
controlled by the first party. No third parties can access this data in any
form. No personal data is included in the analytics data.
If you opt-in to crash logging, we will collect analytics data from your device,
every time a crash occurs. This data includes debugging information such as
devices, versions and the specific error. All data is sent to a server
controlled by the first party. No third parties can access this data in any
form. No personal data is included in the analytics data.

View File

@@ -1,5 +1,6 @@
package_name("nl.moeilijkedingen.jellyfinaudioplayer")
app_identifier("nl.moeilijkedingen.jellyfinaudioplayer")
apple_id("lei@moeilijkedingen.nl")
team_id("238P3C58WC")
json_key_file("./fastlane/play-store-credentials.json")
apple_id("lei@codified.nl")
team_id("HD2D35G9Y4")
json_key_file("./fastlane/play-store-credentials.json")
itc_team_id("127114471")

View File

@@ -253,19 +253,19 @@
TargetAttributes = {
00E356ED1AD99517003FC87E = {
CreatedOnToolsVersion = 6.2;
DevelopmentTeam = 238P3C58WC;
DevelopmentTeam = HD2D35G9Y4;
ProvisioningStyle = Manual;
TestTargetID = 13B07F861A680F5B00A75B9A;
};
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = 238P3C58WC;
DevelopmentTeam = HD2D35G9Y4;
LastSwiftMigration = 1210;
ProvisioningStyle = Manual;
ProvisioningStyle = Automatic;
};
AB4A8DFA2857C8DA005A1ED0 = {
CreatedOnToolsVersion = 13.4.1;
DevelopmentTeam = 238P3C58WC;
ProvisioningStyle = Manual;
DevelopmentTeam = HD2D35G9Y4;
ProvisioningStyle = Automatic;
TestTargetID = 13B07F861A680F5B00A75B9A;
};
};
@@ -518,7 +518,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = 238P3C58WC;
DEVELOPMENT_TEAM = HD2D35G9Y4;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
@@ -549,7 +549,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
DEVELOPMENT_TEAM = 238P3C58WC;
DEVELOPMENT_TEAM = HD2D35G9Y4;
INFOPLIST_FILE = FintunesTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@@ -576,11 +576,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 84;
DEVELOPMENT_TEAM = 238P3C58WC;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 238P3C58WC;
CURRENT_PROJECT_VERSION = 91;
DEVELOPMENT_TEAM = HD2D35G9Y4;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@@ -601,7 +600,6 @@
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.jellyfinaudioplayer;
PRODUCT_NAME = Fintunes;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1707846041";
SWIFT_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -618,9 +616,9 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 84;
DEVELOPMENT_TEAM = 238P3C58WC;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 238P3C58WC;
CURRENT_PROJECT_VERSION = 91;
DEVELOPMENT_TEAM = HD2D35G9Y4;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = HD2D35G9Y4;
INFOPLIST_FILE = Fintunes/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
@@ -637,7 +635,7 @@
PRODUCT_NAME = Fintunes;
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1707846041";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore";
SWIFT_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -788,10 +786,11 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 84;
CURRENT_PROJECT_VERSION = 91;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 238P3C58WC;
DEVELOPMENT_TEAM = HD2D35G9Y4;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
@@ -801,6 +800,7 @@
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.FintunesUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -820,11 +820,12 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 84;
CURRENT_PROJECT_VERSION = 91;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 238P3C58WC;
DEVELOPMENT_TEAM = HD2D35G9Y4;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
@@ -833,6 +834,7 @@
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.FintunesUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 5.0;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.3.2</string>
<string>2.3.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>84</string>
<string>91</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>

View File

@@ -11,9 +11,9 @@ PODS:
- ReactCommon/turbomodule/core (= 0.73.4)
- fmt (6.2.1)
- glog (0.3.5)
- hermes-engine (0.73.3):
- hermes-engine/Pre-built (= 0.73.3)
- hermes-engine/Pre-built (0.73.3)
- hermes-engine (0.73.4):
- hermes-engine/Pre-built (= 0.73.4)
- hermes-engine/Pre-built (0.73.4)
- libevent (2.1.12)
- libwebp (1.3.2):
- libwebp/demux (= 1.3.2)
@@ -1363,7 +1363,7 @@ SPEC CHECKSUMS:
FBReactNativeSpec: d0086a479be91c44ce4687a962956a352d2dc697
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
hermes-engine: 5420539d016f368cd27e008f65f777abd6098c56
hermes-engine: b2669ce35fc4ac14f523b307aff8896799829fe2
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
@@ -1435,4 +1435,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 292b785d92bbff1138baa390bf579dd7147228be
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2

10
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "fintunes",
"version": "2.3.2",
"version": "2.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fintunes",
"version": "2.3.2",
"version": "2.3.3",
"hasInstallScript": true,
"dependencies": {
"@react-native-async-storage/async-storage": "^1.21.0",
@@ -6876,9 +6876,9 @@
}
},
"node_modules/ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz",
"integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ=="
},
"node_modules/is-array-buffer": {
"version": "3.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "fintunes",
"version": "2.3.2",
"version": "2.3.3",
"main": "src/index.js",
"private": true,
"scripts": {

View File

@@ -12,7 +12,7 @@ import DownloadIcon from '@/components/DownloadIcon';
import styled from 'styled-components/native';
import { Text } from '@/components/Typography';
import FastImage from 'react-native-fast-image';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import { ShadowWrapper } from '@/components/Shadow';
import { SafeFlatList } from '@/components/SafeNavigatorView';
import { t } from '@/localisation';

View File

@@ -9,7 +9,7 @@ import { t } from '@/localisation';
import { NavigationProp, StackParams } from '@/screens/types';
import { SubHeader, Text } from '@/components/Typography';
import { ScrollView } from 'react-native-gesture-handler';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import styled from 'styled-components';
import { Dimensions, Pressable } from 'react-native';
import AlbumImage from './components/AlbumImage';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import { SectionList, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, ReactText } from 'react';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import { View } from 'react-native';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { differenceInDays } from 'date-fns';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import { SectionList, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, ReactText } from 'react';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import { Text, View, FlatList, ListRenderItem, RefreshControl } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import { Text, SafeAreaView, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from '@/store';

View File

@@ -1,6 +1,6 @@
import React, { PropsWithChildren, useCallback, useMemo } from 'react';
import { Platform, RefreshControl, StyleSheet, View } from 'react-native';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import styled, { css } from 'styled-components/native';
import { useNavigation } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from '@/store';

View File

@@ -8,7 +8,7 @@ import { Album, AlbumTrack } from '@/store/music/types';
import { FlatList } from 'react-native-gesture-handler';
import TouchableHandler from '@/components/TouchableHandler';
import { useNavigation } from '@react-navigation/native';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
import FastImage from 'react-native-fast-image';
import { t } from '@/localisation';
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';

View File

@@ -15,7 +15,7 @@ import CoverImage from '@/components/CoverImage';
import { queueTrackForDownload, removeDownloadedTrack } from '@/store/downloads/actions';
import usePlayTracks from '@/utility/usePlayTracks';
import { selectIsDownloaded } from '@/store/downloads/selectors';
import { useGetImage } from '@/utility/JellyfinApi';
import { useGetImage } from '@/utility/JellyfinApi/lib';
type Route = RouteProp<StackParams, 'TrackPopupMenu'>;

View File

@@ -1,9 +1,9 @@
import { createAction, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import { AppState } from '@/store';
import { generateTrackUrl } from '@/utility/JellyfinApi';
import { downloadFile, unlink, DocumentDirectoryPath, exists } from 'react-native-fs';
import { DownloadEntity } from './types';
import MimeTypes from '@/utility/MimeTypes';
import { generateTrackUrl } from '@/utility/JellyfinApi/track';
export const downloadAdapter = createEntityAdapter<DownloadEntity>();
@@ -15,12 +15,9 @@ export const failDownload = createAction<{ id: string }>('download/fail');
export const downloadTrack = createAsyncThunk(
'/downloads/track',
async (id: string, { dispatch, getState }) => {
// Get the credentials from the store
const { settings: { jellyfin: credentials } } = (getState() as AppState);
async (id: string, { dispatch }) => {
// Generate the URL we can use to download the file
const url = generateTrackUrl(id as string, credentials);
const url = generateTrackUrl(id);
// Get the content-type from the URL by doing a HEAD-only request
const contentType = (await fetch(url, { method: 'HEAD' })).headers.get('Content-Type');

View File

@@ -87,6 +87,7 @@ const store = configureStore({
export type AppState = ReturnType<typeof reducers> & { _persist: PersistState };
export type AppDispatch = typeof store.dispatch;
export type AsyncThunkAPI = { state: AppState, dispatch: AppDispatch };
export type Store = typeof store;
export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;
export const useAppDispatch: () => AppDispatch = useDispatch;

View File

@@ -1,7 +1,9 @@
import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import { Album, AlbumTrack, Playlist } from './types';
import { AsyncThunkAPI } from '..';
import { retrieveAllAlbums, retrieveAlbumTracks, retrieveRecentAlbums, searchItem, retrieveAlbum, retrieveAllPlaylists, retrievePlaylistTracks } from '@/utility/JellyfinApi';
import { retrieveAllAlbums, retrieveRecentAlbums, retrieveAlbumTracks, retrieveAlbum } from '@/utility/JellyfinApi/album';
import { retrieveAllPlaylists, retrievePlaylistTracks } from '@/utility/JellyfinApi/playlist';
import { searchItem } from '@/utility/JellyfinApi/search';
export const albumAdapter = createEntityAdapter<Album, string>({
selectId: album => album.Id,
@@ -13,10 +15,7 @@ export const albumAdapter = createEntityAdapter<Album, string>({
*/
export const fetchAllAlbums = createAsyncThunk<Album[], undefined, AsyncThunkAPI>(
'/albums/all',
async (empty, thunkAPI) => {
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAllAlbums(credentials) as Promise<Album[]>;
}
retrieveAllAlbums,
);
/**
@@ -24,10 +23,7 @@ export const fetchAllAlbums = createAsyncThunk<Album[], undefined, AsyncThunkAPI
*/
export const fetchRecentAlbums = createAsyncThunk<Album[], number | undefined, AsyncThunkAPI>(
'/albums/recent',
async (numberOfAlbums, thunkAPI) => {
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveRecentAlbums(credentials, numberOfAlbums) as Promise<Album[]>;
}
retrieveRecentAlbums,
);
export const trackAdapter = createEntityAdapter<AlbumTrack, string>({
@@ -40,18 +36,12 @@ export const trackAdapter = createEntityAdapter<AlbumTrack, string>({
*/
export const fetchTracksByAlbum = createAsyncThunk<AlbumTrack[], string, AsyncThunkAPI>(
'/tracks/byAlbum',
async (ItemId, thunkAPI) => {
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAlbumTracks(ItemId, credentials) as Promise<AlbumTrack[]>;
}
retrieveAlbumTracks,
);
export const fetchAlbum = createAsyncThunk<Album, string, AsyncThunkAPI>(
'/albums/single',
async (ItemId, thunkAPI) => {
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAlbum(credentials, ItemId) as Promise<Album>;
}
retrieveAlbum,
);
type SearchAndFetchResults = {
@@ -67,16 +57,17 @@ AsyncThunkAPI
'/search',
async ({ term, limit = 24 }, thunkAPI) => {
const state = thunkAPI.getState();
const results = await searchItem(state.settings.jellyfin, term, limit);
const results = await searchItem(term, limit);
const albums = await Promise.all(results.filter((item) => (
!state.music.albums.ids.includes(item.Type === 'MusicAlbum' ? item.Id : item.AlbumId)
&& (item.Type === 'Audio' ? item.AlbumId : true)
)).map(async (item) => {
if (item.Type === 'MusicAlbum') {
return item;
}
return retrieveAlbum(state.settings.jellyfin, item.AlbumId);
return retrieveAlbum(item.AlbumId);
}));
return {
@@ -96,10 +87,7 @@ export const playlistAdapter = createEntityAdapter<Playlist, string>({
*/
export const fetchAllPlaylists = createAsyncThunk<Playlist[], undefined, AsyncThunkAPI>(
'/playlists/all',
async (empty, thunkAPI) => {
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAllPlaylists(credentials) as Promise<Playlist[]>;
}
retrieveAllPlaylists,
);
/**
@@ -107,8 +95,5 @@ export const fetchAllPlaylists = createAsyncThunk<Playlist[], undefined, AsyncTh
*/
export const fetchTracksByPlaylist = createAsyncThunk<AlbumTrack[], string, AsyncThunkAPI>(
'/tracks/byPlaylist',
async (ItemId, thunkAPI) => {
const credentials = thunkAPI.getState().settings.jellyfin;
return retrievePlaylistTracks(ItemId, credentials) as Promise<AlbumTrack[]>;
}
retrievePlaylistTracks,
);

View File

@@ -15,8 +15,8 @@ export function useRecentAlbums(amount: number) {
const sorted = [...albumIds].sort((a, b) => {
const albumA = albums[a];
const albumB = albums[b];
const dateA = albumA ? parseISO(albumA.DateCreated).getTime() : 0;
const dateB = albumB ? parseISO(albumB.DateCreated).getTime() : 0;
const dateA = albumA && albumA.DateCreated ? parseISO(albumA.DateCreated).getTime() : 0;
const dateB = albumB && albumB.DateCreated ? parseISO(albumB.DateCreated).getTime() : 0;
return dateB - dateA;
});

View File

@@ -1,322 +0,0 @@
import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player';
import { AppState, useTypedSelector } from '@/store';
import { Album, AlbumTrack, SimilarAlbum } from '@/store/music/types';
import { Platform } from 'react-native';
type Credentials = AppState['settings']['jellyfin'];
/**
* This is a convenience function that converts a set of Jellyfin credentials
* from the Redux store to a HTTP Header that authenticates the user against the
* Jellyfin server.
*/
function generateConfig(credentials: Credentials): RequestInit {
return {
headers: {
'X-Emby-Authorization': `MediaBrowser Client="", Device="", DeviceId="", Version="", Token="${credentials?.access_token}"`
}
};
}
const trackOptionsOsOverrides: Record<typeof Platform.OS, Record<string, string>> = {
ios: {
Container: 'mp3,aac,m4a|aac,m4b|aac,flac,alac,m4a|alac,m4b|alac,wav,m4a,aiff,aif',
},
android: {
Container: 'mp3,aac,flac,wav,ogg,ogg|vorbis,ogg|opus,mka|mp3,mka|opus,mka|mp3',
},
macos: {},
web: {},
windows: {},
};
const baseTrackOptions: Record<string, string> = {
TranscodingProtocol: 'http',
TranscodingContainer: 'aac',
AudioCodec: 'aac',
Container: 'mp3,aac',
...trackOptionsOsOverrides[Platform.OS],
};
/**
* Generate a track object from a Jellyfin ItemId so that
* react-native-track-player can easily consume it.
*/
export function generateTrack(track: AlbumTrack, credentials: Credentials): Track {
// Also construct the URL for the stream
const url = generateTrackUrl(track.Id, credentials);
return {
url,
backendId: track.Id,
title: track.Name,
artist: track.Artists.join(', '),
album: track.Album,
duration: track.RunTimeTicks,
artwork: track.AlbumId
? getImage(track.AlbumId, credentials)
: getImage(track.Id, credentials),
};
}
/**
* Generate the track streaming url from the trackId
*/
export function generateTrackUrl(trackId: string, credentials: Credentials) {
const trackOptions = {
...baseTrackOptions,
UserId: credentials?.user_id || '',
api_key: credentials?.access_token || '',
DeviceId: credentials?.device_id || '',
};
const trackParams = new URLSearchParams(trackOptions).toString();
const url = encodeURI(`${credentials?.uri}/Audio/${trackId}/universal?`) + trackParams;
return url;
}
const albumOptions = {
SortBy: 'AlbumArtist,SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'MusicAlbum',
Recursive: 'true',
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
ImageTypeLimit: '1',
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
};
const albumParams = new URLSearchParams(albumOptions).toString();
/**
* Retrieve all albums that are available on the Jellyfin server
*/
export async function retrieveAllAlbums(credentials: Credentials) {
const config = generateConfig(credentials);
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${albumParams}`, config)
.then(response => response.json());
return albums.Items;
}
/**
* Retrieve a single album
*/
export async function retrieveAlbum(credentials: Credentials, id: string): Promise<Album> {
const config = generateConfig(credentials);
const Similar = await fetch(`${credentials?.uri}/Items/${id}/Similar?userId=${credentials?.user_id}&limit=12`, config)
.then(response => response.json() as Promise<{ Items: SimilarAlbum[] }>)
.then((albums) => albums.Items.map((a) => a.Id));
return fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/${id}`, config)
.then(response => response.json() as Promise<Album>)
.then(album => ({ ...album, Similar }));
}
const latestAlbumsOptions = {
IncludeItemTypes: 'MusicAlbum',
Fields: 'DateCreated',
SortOrder: 'Ascending',
};
/**
* Retrieve the most recently added albums on the Jellyfin server
*/
export async function retrieveRecentAlbums(credentials: Credentials, numberOfAlbums = 24) {
const config = generateConfig(credentials);
// Generate custom config based on function input
const options = {
...latestAlbumsOptions,
Limit: numberOfAlbums.toString(),
};
const params = new URLSearchParams(options).toString();
// Retrieve albums
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/Latest?${params}`, config)
.then(response => response.json());
return albums;
}
/**
* Retrieve a single album from the Emby server
*/
export async function retrieveAlbumTracks(ItemId: string, credentials: Credentials) {
const singleAlbumOptions = {
ParentId: ItemId,
SortBy: 'SortName',
};
const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString();
const config = generateConfig(credentials);
const album = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${singleAlbumParams}`, config)
.then(response => response.json());
return album.Items;
}
/**
* Retrieve an image URL for a given ItemId
*/
export function getImage(ItemId: string, credentials: Credentials): string {
return encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`);
}
/**
* Create a hook that can convert ItemIds to image URLs
*/
export function useGetImage() {
const credentials = useTypedSelector((state) => state.settings.jellyfin);
return (ItemId: string) => getImage(ItemId, credentials);
}
const trackParams = {
SortBy: 'AlbumArtist,SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Audio',
Recursive: 'true',
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
};
/**
* Retrieve all possible tracks that can be found in Jellyfin
*/
export async function retrieveAllTracks(credentials: Credentials) {
const config = generateConfig(credentials);
const tracks = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${trackParams}`, config)
.then(response => response.json());
return tracks.Items;
}
const searchParams = {
IncludeItemTypes: 'Audio,MusicAlbum',
SortBy: 'Album,SortName',
SortOrder: 'Ascending',
Recursive: 'true',
};
/**
* Remotely search the Jellyfin library for a particular search term
*/
export async function searchItem(
credentials: Credentials,
term: string, limit = 24
): Promise<(Album | AlbumTrack)[]> {
const config = generateConfig(credentials);
const params = new URLSearchParams({
...searchParams,
SearchTerm: term,
Limit: limit.toString(),
}).toString();
const results = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${params}`, config)
.then(response => response.json());
return results.Items;
}
const playlistOptions = {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Playlist',
Recursive: 'true',
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
ImageTypeLimit: '1',
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
MediaTypes: 'Audio',
};
/**
* Retrieve all albums that are available on the Jellyfin server
*/
export async function retrieveAllPlaylists(credentials: Credentials) {
const config = generateConfig(credentials);
const playlistParams = new URLSearchParams(playlistOptions).toString();
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${playlistParams}`, config)
.then(response => response.json());
return albums.Items;
}
/**
* Retrieve all albums that are available on the Jellyfin server
*/
export async function retrievePlaylistTracks(ItemId: string, credentials: Credentials) {
const singlePlaylistOptions = {
SortBy: 'SortName',
UserId: credentials?.user_id || '',
};
const singlePlaylistParams = new URLSearchParams(singlePlaylistOptions).toString();
const config = generateConfig(credentials);
const playlists = await fetch(`${credentials?.uri}/Playlists/${ItemId}/Items?${singlePlaylistParams}`, config)
.then(response => response.json());
return playlists.Items;
}
/**
* This maps the react-native-track-player RepeatMode to a RepeatMode that is
* expected by Jellyfin when reporting playback events.
*/
const RepeatModeMap: Record<RepeatMode, string> = {
[RepeatMode.Off]: 'RepeatNone',
[RepeatMode.Track]: 'RepeatOne',
[RepeatMode.Queue]: 'RepeatAll',
};
/**
* This will generate the payload that is required for playback events and send
* it to the supplied path.
*/
export async function sendPlaybackEvent(path: string, credentials: Credentials, trackIndex?: number) {
// Extract all data from react-native-track-player
const [
currentTrack, position, repeatMode, volume, queue, state,
] = await Promise.all([
TrackPlayer.getCurrentTrack(),
TrackPlayer.getPosition(),
TrackPlayer.getRepeatMode(),
TrackPlayer.getVolume(),
TrackPlayer.getQueue(),
TrackPlayer.getState(),
]);
// Switch between overriden track index and current track
const track = trackIndex !== undefined ? trackIndex : currentTrack;
// Generate a payload from the gathered data
const payload = {
VolumeLevel: volume * 100,
IsMuted: false,
IsPaused: state === State.Paused,
RepeatMode: RepeatModeMap[repeatMode],
ShuffleMode: 'Sorted',
PositionTicks: Math.round(position * 10_000_000),
PlaybackRate: 1,
PlayMethod: 'transcode',
MediaSourceId: track !== null ? queue[track].backendId : null,
ItemId: track !== null ? queue[track].backendId : null,
CanSeek: true,
PlaybackStartTimeTicks: null,
};
// Generate a config from the credentials and dispatch the request
const config = generateConfig(credentials);
await fetch(`${credentials?.uri}${path}`, {
method: 'POST',
headers: {
...config.headers,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
// Swallow and errors from the request
}).catch((err) => {
console.error(err);
});
}

View File

@@ -0,0 +1,68 @@
import { Album, AlbumTrack, SimilarAlbum } from '@/store/music/types';
import { fetchApi } from './lib';
const albumOptions = {
SortBy: 'AlbumArtist,SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'MusicAlbum',
Recursive: 'true',
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
ImageTypeLimit: '1',
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
};
const albumParams = new URLSearchParams(albumOptions).toString();
/**
* Retrieve all albums that are available on the Jellyfin server
*/
export async function retrieveAllAlbums() {
return fetchApi<{ Items: Album[] }>(({ user_id }) => `/Users/${user_id}/Items?${albumParams}`)
.then((data) => data.Items);
}
/**
* Retrieve a single album
*/
export async function retrieveAlbum(id: string): Promise<Album> {
const Similar = await fetchApi<{ Items: SimilarAlbum[] }>(({ user_id }) => `/Items/${id}/Similar?userId=${user_id}&limit=12`)
.then((albums) => albums.Items.map((a) => a.Id));
return fetchApi<Album>(({ user_id }) => `/Users/${user_id}/Items/${id}`)
.then(album => ({ ...album, Similar }));
}
const latestAlbumsOptions = {
IncludeItemTypes: 'MusicAlbum',
Fields: 'DateCreated',
SortOrder: 'Ascending',
};
/**
* Retrieve the most recently added albums on the Jellyfin server
*/
export async function retrieveRecentAlbums(numberOfAlbums = 24) {
// Generate custom config based on function input
const options = {
...latestAlbumsOptions,
Limit: numberOfAlbums.toString(),
};
const params = new URLSearchParams(options).toString();
// Retrieve albums
return fetchApi<Album[]>(({ user_id }) => `/Users/${user_id}/Items/Latest?${params}`);
}
/**
* Retrieve a single album from the Emby server
*/
export async function retrieveAlbumTracks(ItemId: string) {
const singleAlbumOptions = {
ParentId: ItemId,
SortBy: 'SortName',
};
const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString();
return fetchApi<{ Items: AlbumTrack[] }>(({ user_id }) => `/Users/${user_id}/Items?${singleAlbumParams}`)
.then((data) => data.Items);
}

View File

@@ -0,0 +1,87 @@
import type { AppState, Store } from '@/store';
type Credentials = AppState['settings']['jellyfin'];
/**
* This is a convenience function that converts a set of Jellyfin credentials
* from the Redux store to a HTTP Header that authenticates the user against the
* Jellyfin server.
*/
function generateConfig(credentials: Credentials): RequestInit {
return {
headers: {
'X-Emby-Authorization': `MediaBrowser Client="", Device="", DeviceId="", Version="", Token="${credentials?.access_token}"`
}
};
}
export function asyncFetchStore() {
return require('@/store').default as Store;
}
/**
* A convenience function that accepts a request for fetch, injects it with the
* proper Jellyfin credentials and attempts to catch any errors along the way.
*/
export async function fetchApi<T>(
path: string | ((credentials: NonNullable<Credentials>) => string),
config?: RequestInit
) {
// Retrieve the latest credentials from the Redux store
const credentials = asyncFetchStore().getState().settings.jellyfin;
// GUARD: Check that the credentials are present
if (!credentials) {
throw new Error('Missing Jellyfin credentials when attempting API request');
}
// Create the URL from the path and the credentials
const resolvedPath = typeof path === 'function' ? path(credentials) : path;
const url = `${credentials.uri}${resolvedPath.startsWith('/') ? '' : '/'}${resolvedPath}`;
// Actually perform the request
const response = await fetch(url, {
...config,
headers: {
...config?.headers,
...generateConfig(credentials).headers,
}
});
// GUARD: Check if the response is as expected
if (!response.ok) {
if (response.status === 403 || response.status === 401) {
throw new Error('AuthenticationFailed');
} else if (response.status === 404) {
throw new Error('ResourceNotFound');
}
// Attempt to parse the error message
try {
const data = await response.json();
throw data;
} catch {
throw response;
}
}
// Parse body as JSON
const data = await response.json() as Promise<T>;
return data;
}
/**
* Retrieve an image URL for a given ItemId
*/
export function getImage(ItemId: string): string {
const credentials = asyncFetchStore().getState().settings.jellyfin;
return encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`);
}
/**
* Create a hook that can convert ItemIds to image URLs
*/
export function useGetImage() {
return (ItemId: string) => getImage(ItemId);
}

View File

@@ -0,0 +1,60 @@
import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player';
import { fetchApi } from './lib';
/**
* This maps the react-native-track-player RepeatMode to a RepeatMode that is
* expected by Jellyfin when reporting playback events.
*/
const RepeatModeMap: Record<RepeatMode, string> = {
[RepeatMode.Off]: 'RepeatNone',
[RepeatMode.Track]: 'RepeatOne',
[RepeatMode.Queue]: 'RepeatAll',
};
/**
* This will generate the payload that is required for playback events and send
* it to the supplied path.
*/
export async function sendPlaybackEvent(
path: string,
track?: Track
) {
// Extract all data from react-native-track-player
const [
activeTrack, { position }, repeatMode, volume, { state },
] = await Promise.all([
track || TrackPlayer.getActiveTrack(),
TrackPlayer.getProgress(),
TrackPlayer.getRepeatMode(),
TrackPlayer.getVolume(),
TrackPlayer.getPlaybackState(),
]);
// Generate a payload from the gathered data
const payload = {
VolumeLevel: volume * 100,
IsMuted: false,
IsPaused: state === State.Paused,
RepeatMode: RepeatModeMap[repeatMode],
ShuffleMode: 'Sorted',
PositionTicks: Math.round(position * 10_000_000),
PlaybackRate: 1,
PlayMethod: 'transcode',
MediaSourceId: activeTrack?.backendId || null,
ItemId: activeTrack?.backendId || null,
CanSeek: true,
PlaybackStartTimeTicks: null,
};
// Generate a config from the credentials and dispatch the request
await fetchApi(path, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
// Swallow and errors from the request
}).catch((err) => {
console.error(err);
});
}

View File

@@ -0,0 +1,38 @@
import { AlbumTrack, Playlist } from '@/store/music/types';
import { asyncFetchStore, fetchApi } from './lib';
const playlistOptions = {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Playlist',
Recursive: 'true',
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
ImageTypeLimit: '1',
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
MediaTypes: 'Audio',
};
/**
* Retrieve all albums that are available on the Jellyfin server
*/
export async function retrieveAllPlaylists() {
const playlistParams = new URLSearchParams(playlistOptions).toString();
return fetchApi<{ Items: Playlist[] }>(({ user_id }) => `/Users/${user_id}/Items?${playlistParams}`)
.then((d) => d.Items);
}
/**
* Retrieve all albums that are available on the Jellyfin server
*/
export async function retrievePlaylistTracks(ItemId: string) {
const credentials = asyncFetchStore().getState().settings.jellyfin;
const singlePlaylistOptions = {
SortBy: 'SortName',
UserId: credentials?.user_id || '',
};
const singlePlaylistParams = new URLSearchParams(singlePlaylistOptions).toString();
return fetchApi<{ Items: AlbumTrack[] }>(`/Playlists/${ItemId}/Items?${singlePlaylistParams}`)
.then((d) => d.Items);
}

View File

@@ -0,0 +1,30 @@
import { Album, AlbumTrack } from '@/store/music/types';
import { fetchApi } from './lib';
const searchParams = {
IncludeItemTypes: 'Audio,MusicAlbum',
SortBy: 'Album,SortName',
SortOrder: 'Ascending',
Recursive: 'true',
};
/**
* Remotely search the Jellyfin library for a particular search term
*/
export async function searchItem(
term: string, limit = 24
) {
const params = new URLSearchParams({
...searchParams,
SearchTerm: term,
Limit: limit.toString(),
}).toString();
const results = await fetchApi<{ Items: (Album | AlbumTrack)[]}>(({ user_id }) => `/Users/${user_id}/Items?${params}`);
return results.Items
.filter((item) => (
// GUARD: Ensure that we're either dealing with an album or a track from an album.
item.Type === 'MusicAlbum' || (item.Type === 'Audio' && item.AlbumId)
));
}

View File

@@ -0,0 +1,81 @@
import { AlbumTrack } from '@/store/music/types';
import { Platform } from 'react-native';
import { Track } from 'react-native-track-player';
import { fetchApi, getImage } from './lib';
import store from '@/store';
const trackOptionsOsOverrides: Record<typeof Platform.OS, Record<string, string>> = {
ios: {
Container: 'mp3,aac,m4a|aac,m4b|aac,flac,alac,m4a|alac,m4b|alac,wav,m4a,aiff,aif',
},
android: {
Container: 'mp3,aac,flac,wav,ogg,ogg|vorbis,ogg|opus,mka|mp3,mka|opus,mka|mp3',
},
macos: {},
web: {},
windows: {},
};
const baseTrackOptions: Record<string, string> = {
TranscodingProtocol: 'http',
TranscodingContainer: 'aac',
AudioCodec: 'aac',
Container: 'mp3,aac',
...trackOptionsOsOverrides[Platform.OS],
};
/**
* Generate the track streaming url from the trackId
*/
export function generateTrackUrl(trackId: string) {
const credentials = store.getState().settings.jellyfin;
const trackOptions = {
...baseTrackOptions,
UserId: credentials?.user_id || '',
api_key: credentials?.access_token || '',
DeviceId: credentials?.device_id || '',
};
const trackParams = new URLSearchParams(trackOptions).toString();
const url = encodeURI(`${credentials?.uri}/Audio/${trackId}/universal?`) + trackParams;
return url;
}
/**
* Generate a track object from a Jellyfin ItemId so that
* react-native-track-player can easily consume it.
*/
export function generateTrack(track: AlbumTrack): Track {
// Also construct the URL for the stream
const url = generateTrackUrl(track.Id);
return {
url,
backendId: track.Id,
title: track.Name,
artist: track.Artists.join(', '),
album: track.Album,
duration: track.RunTimeTicks,
artwork: track.AlbumId
? getImage(track.AlbumId)
: getImage(track.Id),
};
}
const trackParams = {
SortBy: 'AlbumArtist,SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Audio',
Recursive: 'true',
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
};
/**
* Retrieve all possible tracks that can be found in Jellyfin
*/
export async function retrieveAllTracks() {
return fetchApi<{ Items: AlbumTrack[] }>(({ user_id }) => `/Users/${user_id}/Items?${trackParams}`)
.then((d) => d.Items);
}

View File

@@ -9,8 +9,8 @@
import TrackPlayer, { Event, State } from 'react-native-track-player';
import store from '@/store';
import { sendPlaybackEvent } from './JellyfinApi';
import { setTimerDate } from '@/store/sleep-timer';
import { sendPlaybackEvent } from './JellyfinApi/playback';
export default async function() {
TrackPlayer.addEventListener(Event.RemotePlay, () => {
@@ -37,18 +37,18 @@ export default async function() {
TrackPlayer.seekTo(event.position);
});
TrackPlayer.addEventListener(Event.PlaybackTrackChanged, async (e) => {
TrackPlayer.addEventListener(Event.PlaybackActiveTrackChanged, async (e) => {
// Retrieve the current settings from the Redux store
const settings = store.getState().settings;
// GUARD: Only report playback when the settings is enabled
if (settings.enablePlaybackReporting && 'track' in e) {
// GUARD: End the previous track if it's about to end
if ('nextTrack' in e && typeof e.track === 'number') {
sendPlaybackEvent('/Sessions/Playing/Stopped', settings.jellyfin, e.track);
if (e.lastTrack) {
await sendPlaybackEvent('/Sessions/Playing/Stopped', e.lastTrack);
}
sendPlaybackEvent('/Sessions/Playing', settings.jellyfin);
await sendPlaybackEvent('/Sessions/Playing', e.track);
}
});
@@ -58,7 +58,7 @@ export default async function() {
// GUARD: Only report playback when the settings is enabled
if (settings.enablePlaybackReporting) {
sendPlaybackEvent('/Sessions/Playing/Progress', settings.jellyfin);
sendPlaybackEvent('/Sessions/Playing/Progress');
}
// check if timerDate is undefined, otherwise start timer
@@ -69,14 +69,16 @@ export default async function() {
});
TrackPlayer.addEventListener(Event.PlaybackState, (event) => {
// GUARD: Only respond to stopped events
if (event.state === State.Stopped) {
// Retrieve the current settings from the Redux store
const settings = store.getState().settings;
// Retrieve the current settings from the Redux store
const settings = store.getState().settings;
// GUARD: Only report playback when the settings is enabled
if (settings.enablePlaybackReporting) {
sendPlaybackEvent('/Sessions/Playing/Stopped', settings.jellyfin);
// GUARD: Only report playback when the settings is enabled
if (settings.enablePlaybackReporting) {
// GUARD: Only respond to stopped events
if (event.state === State.Stopped) {
sendPlaybackEvent('/Sessions/Playing/Stopped');
} else if (event.state === State.Paused) {
sendPlaybackEvent('/Sessions/Playing/Progress');
}
}
});

View File

@@ -1,8 +1,8 @@
import { useTypedSelector } from '@/store';
import { useCallback } from 'react';
import TrackPlayer, { Track } from 'react-native-track-player';
import { generateTrack } from './JellyfinApi';
import { shuffle as shuffleArray } from 'lodash';
import { generateTrack } from './JellyfinApi/track';
interface PlayOptions {
play: boolean;
@@ -21,7 +21,6 @@ const defaults: PlayOptions = {
* supplied id.
*/
export default function usePlayTracks() {
const credentials = useTypedSelector(state => state.settings.jellyfin);
const tracks = useTypedSelector(state => state.music.tracks.entities);
const downloads = useTypedSelector(state => state.downloads.entities);
@@ -51,7 +50,7 @@ export default function usePlayTracks() {
}
// Retrieve the generated track from Jellyfin
const generatedTrack = generateTrack(track, credentials);
const generatedTrack = generateTrack(track);
// Check if a downloaded version exists, and if so rewrite the URL
const download = downloads[trackId];
@@ -114,5 +113,5 @@ export default function usePlayTracks() {
break;
}
}
}, [credentials, downloads, tracks]);
}, [downloads, tracks]);
}