Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a97611c0ad | ||
|
|
e511f744ad | ||
|
|
a6a306b5be | ||
|
|
881ab95029 | ||
|
|
968e98d8df | ||
|
|
b01470bde8 | ||
|
|
823f7b59e8 | ||
|
|
16162d8e35 | ||
|
|
ea817025e1 | ||
|
|
00675bbbd3 | ||
|
|
24b5a47a7c | ||
|
|
bb655cb719 | ||
|
|
be0c7002ff | ||
|
|
e472d043cf | ||
|
|
366d16c485 | ||
|
|
845eac70a0 | ||
|
|
c9662769fa |
34
CHANGELOG.md
34
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}/.."
|
||||
)
|
||||
|
||||
112
ios/Podfile.lock
112
ios/Podfile.lock
@@ -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
10
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fintunes",
|
||||
"version": "2.3.1",
|
||||
"version": "2.3.3",
|
||||
"main": "src/index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
13
patches/@react-native-community+datetimepicker+7.6.2.patch
Normal file
13
patches/@react-native-community+datetimepicker+7.6.2.patch
Normal 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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
68
src/utility/JellyfinApi/album.ts
Normal file
68
src/utility/JellyfinApi/album.ts
Normal 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);
|
||||
}
|
||||
87
src/utility/JellyfinApi/lib.ts
Normal file
87
src/utility/JellyfinApi/lib.ts
Normal 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);
|
||||
}
|
||||
60
src/utility/JellyfinApi/playback.ts
Normal file
60
src/utility/JellyfinApi/playback.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
38
src/utility/JellyfinApi/playlist.ts
Normal file
38
src/utility/JellyfinApi/playlist.ts
Normal 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);
|
||||
}
|
||||
30
src/utility/JellyfinApi/search.ts
Normal file
30
src/utility/JellyfinApi/search.ts
Normal 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)
|
||||
));
|
||||
}
|
||||
81
src/utility/JellyfinApi/track.ts
Normal file
81
src/utility/JellyfinApi/track.ts
Normal 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);
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user