Compare commits

..

17 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
Lei Nelissen
be0c7002ff chore: also release v2.3.2 for Android 2024-03-10 23:12:10 +01: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
Lei Nelissen
366d16c485 chore: release v2.3.2 2024-03-10 23:08:34 +01:00
Lei Nelissen
845eac70a0 fix: build with xcode 15.3 2024-03-10 22:54:02 +01:00
Lei Nelissen
c9662769fa fix: remove conflicting app transport properties 2024-03-10 22:53:46 +01:00
35 changed files with 518 additions and 550 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 26
versionName "2.3.1"
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;
};
};
@@ -341,16 +341,10 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
@@ -365,16 +359,10 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
@@ -530,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)",
@@ -561,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";
@@ -588,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 = 83;
DEVELOPMENT_TEAM = 238P3C58WC;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 238P3C58WC;
CURRENT_PROJECT_VERSION = 91;
DEVELOPMENT_TEAM = HD2D35G9Y4;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@@ -613,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;
@@ -630,9 +616,9 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 83;
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 = (
@@ -649,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";
@@ -800,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 = 83;
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;
@@ -813,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";
@@ -832,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 = 83;
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;
@@ -845,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,19 +17,19 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.3.1</string>
<string>2.3.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>83</string>
<string>91</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
@@ -42,6 +42,8 @@
<string>audio</string>
<string>fetch</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
@@ -54,13 +56,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UISupportsDocumentBrowser</key>
<true/>
</dict>
</plist>

View File

@@ -34,7 +34,7 @@ target 'Fintunes' do
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable the next line.
:flipper_configuration => flipper_config,
# :flipper_configuration => flipper_config,
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)

View File

@@ -1,6 +1,5 @@
PODS:
- boost (1.83.0)
- CocoaAsyncSocket (7.6.5)
- DoubleConversion (1.1.6)
- FBLazyVector (0.73.4)
- FBReactNativeSpec (0.73.4):
@@ -10,67 +9,11 @@ PODS:
- React-Core (= 0.73.4)
- React-jsi (= 0.73.4)
- ReactCommon/turbomodule/core (= 0.73.4)
- Flipper (0.201.0):
- Flipper-Folly (~> 2.6)
- Flipper-Boost-iOSX (1.76.0.1.11)
- Flipper-DoubleConversion (3.2.0.1)
- Flipper-Fmt (7.1.7)
- Flipper-Folly (2.6.10):
- Flipper-Boost-iOSX
- Flipper-DoubleConversion
- Flipper-Fmt (= 7.1.7)
- Flipper-Glog
- libevent (~> 2.1.12)
- OpenSSL-Universal (= 1.1.1100)
- Flipper-Glog (0.5.0.5)
- Flipper-PeerTalk (0.0.4)
- FlipperKit (0.201.0):
- FlipperKit/Core (= 0.201.0)
- FlipperKit/Core (0.201.0):
- Flipper (~> 0.201.0)
- FlipperKit/CppBridge
- FlipperKit/FBCxxFollyDynamicConvert
- FlipperKit/FBDefines
- FlipperKit/FKPortForwarding
- SocketRocket (~> 0.6.0)
- FlipperKit/CppBridge (0.201.0):
- Flipper (~> 0.201.0)
- FlipperKit/FBCxxFollyDynamicConvert (0.201.0):
- Flipper-Folly (~> 2.6)
- FlipperKit/FBDefines (0.201.0)
- FlipperKit/FKPortForwarding (0.201.0):
- CocoaAsyncSocket (~> 7.6)
- Flipper-PeerTalk (~> 0.0.4)
- FlipperKit/FlipperKitHighlightOverlay (0.201.0)
- FlipperKit/FlipperKitLayoutHelpers (0.201.0):
- FlipperKit/Core
- FlipperKit/FlipperKitHighlightOverlay
- FlipperKit/FlipperKitLayoutTextSearchable
- FlipperKit/FlipperKitLayoutIOSDescriptors (0.201.0):
- FlipperKit/Core
- FlipperKit/FlipperKitHighlightOverlay
- FlipperKit/FlipperKitLayoutHelpers
- FlipperKit/FlipperKitLayoutPlugin (0.201.0):
- FlipperKit/Core
- FlipperKit/FlipperKitHighlightOverlay
- FlipperKit/FlipperKitLayoutHelpers
- FlipperKit/FlipperKitLayoutIOSDescriptors
- FlipperKit/FlipperKitLayoutTextSearchable
- FlipperKit/FlipperKitLayoutTextSearchable (0.201.0)
- FlipperKit/FlipperKitNetworkPlugin (0.201.0):
- FlipperKit/Core
- FlipperKit/FlipperKitReactPlugin (0.201.0):
- FlipperKit/Core
- FlipperKit/FlipperKitUserDefaultsPlugin (0.201.0):
- FlipperKit/Core
- FlipperKit/SKIOSNetworkPlugin (0.201.0):
- FlipperKit/Core
- FlipperKit/FlipperKitNetworkPlugin
- 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)
@@ -84,7 +27,6 @@ PODS:
- libwebp/sharpyuv (1.3.2)
- libwebp/webp (1.3.2):
- libwebp/sharpyuv
- OpenSSL-Universal (1.1.1100)
- RCT-Folly (2022.05.16.00):
- boost
- DoubleConversion
@@ -1200,31 +1142,10 @@ DEPENDENCIES:
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
- Flipper (= 0.201.0)
- Flipper-Boost-iOSX (= 1.76.0.1.11)
- Flipper-DoubleConversion (= 3.2.0.1)
- Flipper-Fmt (= 7.1.7)
- Flipper-Folly (= 2.6.10)
- Flipper-Glog (= 0.5.0.5)
- Flipper-PeerTalk (= 0.0.4)
- FlipperKit (= 0.201.0)
- FlipperKit/Core (= 0.201.0)
- FlipperKit/CppBridge (= 0.201.0)
- FlipperKit/FBCxxFollyDynamicConvert (= 0.201.0)
- FlipperKit/FBDefines (= 0.201.0)
- FlipperKit/FKPortForwarding (= 0.201.0)
- FlipperKit/FlipperKitHighlightOverlay (= 0.201.0)
- FlipperKit/FlipperKitLayoutPlugin (= 0.201.0)
- FlipperKit/FlipperKitLayoutTextSearchable (= 0.201.0)
- FlipperKit/FlipperKitNetworkPlugin (= 0.201.0)
- FlipperKit/FlipperKitReactPlugin (= 0.201.0)
- FlipperKit/FlipperKitUserDefaultsPlugin (= 0.201.0)
- FlipperKit/SKIOSNetworkPlugin (= 0.201.0)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- libevent (~> 2.1.12)
- libwebp (= 1.3.2)
- OpenSSL-Universal (= 1.1.1100)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
@@ -1234,7 +1155,6 @@ DEPENDENCIES:
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
- React-Codegen (from `build/generated/ios`)
- React-Core (from `../node_modules/react-native/`)
- React-Core/DevSupport (from `../node_modules/react-native/`)
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
- React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
- React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
@@ -1291,19 +1211,9 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- CocoaAsyncSocket
- Flipper
- Flipper-Boost-iOSX
- Flipper-DoubleConversion
- Flipper-Fmt
- Flipper-Folly
- Flipper-Glog
- Flipper-PeerTalk
- FlipperKit
- fmt
- libevent
- libwebp
- OpenSSL-Universal
- SDWebImage
- SDWebImageWebPCoder
- Sentry
@@ -1448,24 +1358,14 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
FBLazyVector: 84f6edbe225f38aebd9deaf1540a4160b1f087d7
FBReactNativeSpec: d0086a479be91c44ce4687a962956a352d2dc697
Flipper: c7a0093234c4bdd456e363f2f19b2e4b27652d44
Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c
Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30
Flipper-Fmt: 60cbdd92fc254826e61d669a5d87ef7015396a9b
Flipper-Folly: 584845625005ff068a6ebf41f857f468decd26b3
Flipper-Glog: 70c50ce58ddaf67dc35180db05f191692570f446
Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9
FlipperKit: 37525a5d056ef9b93d1578e04bc3ea1de940094f
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
hermes-engine: 5420539d016f368cd27e008f65f777abd6098c56
hermes-engine: b2669ce35fc4ac14f523b307aff8896799829fe2
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
RCTRequired: ab7f915c15569f04a49669e573e6e319a53f9faa
RCTTypeSafety: 63b97ced7b766865057e7154db0e81ce4ee6cf1e
@@ -1533,6 +1433,6 @@ SPEC CHECKSUMS:
SwiftAudioEx: 6f511018b7a0fdfd14ed1bb4081f953588245cc0
Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312
PODFILE CHECKSUM: 0061941757351ef20fb9ad4bc98e51b1212f87d3
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.1",
"version": "2.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fintunes",
"version": "2.3.1",
"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.1",
"version": "2.3.3",
"main": "src/index.js",
"private": true,
"scripts": {

View File

@@ -0,0 +1,13 @@
diff --git a/node_modules/@react-native-community/datetimepicker/ios/RNDateTimePickerShadowView.m b/node_modules/@react-native-community/datetimepicker/ios/RNDateTimePickerShadowView.m
index c139440..4ff3362 100644
--- a/node_modules/@react-native-community/datetimepicker/ios/RNDateTimePickerShadowView.m
+++ b/node_modules/@react-native-community/datetimepicker/ios/RNDateTimePickerShadowView.m
@@ -41,7 +41,7 @@
YGNodeMarkDirty(self.yogaNode);
}
-static YGSize RNDateTimePickerShadowViewMeasure(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
+static YGSize RNDateTimePickerShadowViewMeasure(YGNodeConstRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
{
RNDateTimePickerShadowView *shadowPickerView = (__bridge RNDateTimePickerShadowView *)YGNodeGetContext(node);

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