✨ Netease backend support
This commit is contained in:
		| @@ -8,10 +8,10 @@ Their original app is good enough. But I just want to redesign the user interfac | ||||
| ## Roadmap | ||||
|  | ||||
| - [x] Playing music | ||||
|     - [ ] Add netease music as source | ||||
|     - [x] Add netease music as source | ||||
| - [x] Re-design user interface | ||||
|     - [x] Simplified UI and UX | ||||
|     - [ ] Support for large screen device | ||||
|     - [x] Support for large screen device | ||||
|  | ||||
| ## License | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> | ||||
|     <uses-permission android:name="android.permission.INTERNET"/> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK" /> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ class ErrorNotifier extends GetxController { | ||||
|   Rx<MaterialBanner?> showing = Rx(null); | ||||
|  | ||||
|   void logError(String msg, {StackTrace? trace}) { | ||||
|     log('$msg${trace != null ? '\nTrace:\ntrace' : ''}'); | ||||
|     log('$msg${trace != null ? '\nTrace:\n$trace' : ''}'); | ||||
|     showError(msg); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -141,6 +141,10 @@ class UserPreferencesProvider extends GetxController { | ||||
|     setData(PreferencesTableCompanion(locale: Value(locale))); | ||||
|   } | ||||
|  | ||||
|   void setNeteaseApiInstance(String instance) { | ||||
|     setData(PreferencesTableCompanion(neteaseApiInstance: Value(instance))); | ||||
|   } | ||||
|  | ||||
|   void setPipedInstance(String instance) { | ||||
|     setData(PreferencesTableCompanion(pipedInstance: Value(instance))); | ||||
|   } | ||||
| @@ -161,10 +165,6 @@ class UserPreferencesProvider extends GetxController { | ||||
|     setData(PreferencesTableCompanion(systemTitleBar: Value(isSystemTitleBar))); | ||||
|   } | ||||
|  | ||||
|   void setDiscordPresence(bool discordPresence) { | ||||
|     setData(PreferencesTableCompanion(discordPresence: Value(discordPresence))); | ||||
|   } | ||||
|  | ||||
|   void setNormalizeAudio(bool normalize) { | ||||
|     setData(PreferencesTableCompanion(normalizeAudio: Value(normalize))); | ||||
|     audioPlayer.setAudioNormalization(normalize); | ||||
|   | ||||
| @@ -71,14 +71,16 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final customEndpoint = | ||||
|         CustomSpotifyEndpoints(_auth.auth.value?.accessToken.value ?? ''); | ||||
|     final forYouView = await customEndpoint.getView( | ||||
|       'made-for-x-hub', | ||||
|       market: market, | ||||
|       locale: Intl.canonicalizedLocale(locale.toString()), | ||||
|     ); | ||||
|     _forYouView = forYouView['content']?['items']; | ||||
|     if (_auth.auth.value != null) { | ||||
|       final customEndpoint = | ||||
|           CustomSpotifyEndpoints(_auth.auth.value?.accessToken.value ?? ''); | ||||
|       final forYouView = await customEndpoint.getView( | ||||
|         'made-for-x-hub', | ||||
|         market: market, | ||||
|         locale: Intl.canonicalizedLocale(locale.toString()), | ||||
|       ); | ||||
|       _forYouView = forYouView['content']?['items']; | ||||
|     } | ||||
|     if (mounted) { | ||||
|       setState(() => _isLoading['forYou'] = false); | ||||
|     } else { | ||||
|   | ||||
| @@ -69,301 +69,310 @@ class _PlayerScreenState extends State<PlayerScreen> { | ||||
|         Navigator.of(context).pop(); | ||||
|       }, | ||||
|       direction: DismissiblePageDismissDirection.down, | ||||
|       child: Material( | ||||
|         color: Colors.transparent, | ||||
|         child: SafeArea( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               Expanded( | ||||
|                 child: ListView( | ||||
|                   shrinkWrap: true, | ||||
|                   padding: const EdgeInsets.symmetric(vertical: 24), | ||||
|                   children: [ | ||||
|                     Obx( | ||||
|                       () => LimitedBox( | ||||
|                         maxHeight: maxAlbumSize, | ||||
|                         maxWidth: maxAlbumSize, | ||||
|                         child: Hero( | ||||
|                           tag: const Key('current-active-track-album-art'), | ||||
|                           child: AspectRatio( | ||||
|                             aspectRatio: 1, | ||||
|                             child: ClipRRect( | ||||
|                               borderRadius: | ||||
|                                   const BorderRadius.all(Radius.circular(16)), | ||||
|                               child: _albumArt != null | ||||
|                                   ? AutoCacheImage( | ||||
|                                       _albumArt!, | ||||
|                                       width: albumSize, | ||||
|                                       height: albumSize, | ||||
|                                     ) | ||||
|                                   : Container( | ||||
|                                       color: Theme.of(context) | ||||
|                                           .colorScheme | ||||
|                                           .surfaceContainerHigh, | ||||
|                                       width: 64, | ||||
|                                       height: 64, | ||||
|                                       child: const Center( | ||||
|                                           child: Icon(Icons.image)), | ||||
|                                     ), | ||||
|                             ), | ||||
|                           ).marginSymmetric(horizontal: 24), | ||||
|       child: Scaffold( | ||||
|         backgroundColor: Colors.transparent, | ||||
|         body: SafeArea( | ||||
|           child: Center( | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 Expanded( | ||||
|                   child: ListView( | ||||
|                     shrinkWrap: true, | ||||
|                     padding: const EdgeInsets.symmetric(vertical: 24), | ||||
|                     children: [ | ||||
|                       Obx( | ||||
|                         () => LimitedBox( | ||||
|                           maxHeight: maxAlbumSize, | ||||
|                           maxWidth: maxAlbumSize, | ||||
|                           child: Hero( | ||||
|                             tag: const Key('current-active-track-album-art'), | ||||
|                             child: AspectRatio( | ||||
|                               aspectRatio: 1, | ||||
|                               child: ClipRRect( | ||||
|                                 borderRadius: | ||||
|                                     const BorderRadius.all(Radius.circular(16)), | ||||
|                                 child: _albumArt != null | ||||
|                                     ? AutoCacheImage( | ||||
|                                         _albumArt!, | ||||
|                                         width: albumSize, | ||||
|                                         height: albumSize, | ||||
|                                       ) | ||||
|                                     : Container( | ||||
|                                         color: Theme.of(context) | ||||
|                                             .colorScheme | ||||
|                                             .surfaceContainerHigh, | ||||
|                                         width: 64, | ||||
|                                         height: 64, | ||||
|                                         child: const Center( | ||||
|                                           child: Icon(Icons.image), | ||||
|                                         ), | ||||
|                                       ), | ||||
|                               ), | ||||
|                             ).marginSymmetric(horizontal: 24), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     const Gap(24), | ||||
|                     Obx( | ||||
|                       () => Row( | ||||
|                         mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                         children: [ | ||||
|                           Expanded( | ||||
|                             child: Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       const Gap(24), | ||||
|                       Obx( | ||||
|                         () => Row( | ||||
|                           mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                           children: [ | ||||
|                             Expanded( | ||||
|                               child: Column( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                 children: [ | ||||
|                                   Text( | ||||
|                                     _playback.state.value.activeTrack?.name ?? | ||||
|                                         'Not playing', | ||||
|                                     style: | ||||
|                                         Theme.of(context).textTheme.titleLarge, | ||||
|                                     textAlign: TextAlign.left, | ||||
|                                   ), | ||||
|                                   Text( | ||||
|                                     _playback.state.value.activeTrack?.artists | ||||
|                                             ?.asString() ?? | ||||
|                                         'No author', | ||||
|                                     style: | ||||
|                                         Theme.of(context).textTheme.bodyMedium, | ||||
|                                     overflow: TextOverflow.ellipsis, | ||||
|                                     textAlign: TextAlign.left, | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                             if (_playback.state.value.activeTrack != null && | ||||
|                                 _auth.auth.value != null) | ||||
|                               TrackHeartButton( | ||||
|                                 key: ValueKey( | ||||
|                                   _playback.state.value.activeTrack!.id!, | ||||
|                                 ), | ||||
|                                 trackId: _playback.state.value.activeTrack!.id!, | ||||
|                               ), | ||||
|                           ], | ||||
|                         ).paddingSymmetric(horizontal: 32), | ||||
|                       ), | ||||
|                       const Gap(24), | ||||
|                       Obx( | ||||
|                         () => Column( | ||||
|                           children: [ | ||||
|                             SliderTheme( | ||||
|                               data: SliderThemeData( | ||||
|                                 trackHeight: 2, | ||||
|                                 trackShape: _PlayerProgressTrackShape(), | ||||
|                                 thumbShape: const RoundSliderThumbShape( | ||||
|                                   enabledThumbRadius: 8, | ||||
|                                 ), | ||||
|                                 overlayShape: SliderComponentShape.noOverlay, | ||||
|                               ), | ||||
|                               child: Slider( | ||||
|                                 secondaryTrackValue: _playback | ||||
|                                     .durationBuffered.value.inMilliseconds | ||||
|                                     .abs() | ||||
|                                     .toDouble(), | ||||
|                                 value: _draggingValue?.abs() ?? | ||||
|                                     _playback | ||||
|                                         .durationCurrent.value.inMilliseconds | ||||
|                                         .toDouble() | ||||
|                                         .abs(), | ||||
|                                 min: 0, | ||||
|                                 max: max( | ||||
|                                   _playback.durationCurrent.value.inMilliseconds | ||||
|                                       .abs(), | ||||
|                                   _playback.durationTotal.value.inMilliseconds | ||||
|                                       .abs(), | ||||
|                                 ).toDouble(), | ||||
|                                 onChanged: (value) { | ||||
|                                   setState(() => _draggingValue = value); | ||||
|                                 }, | ||||
|                                 onChangeEnd: (value) { | ||||
|                                   audioPlayer.seek( | ||||
|                                     Duration(milliseconds: value.toInt()), | ||||
|                                   ); | ||||
|                                   setState(() => _draggingValue = null); | ||||
|                                 }, | ||||
|                               ), | ||||
|                             ), | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
|                                 Text( | ||||
|                                   _playback.state.value.activeTrack?.name ?? | ||||
|                                       'Not playing', | ||||
|                                   style: Theme.of(context).textTheme.titleLarge, | ||||
|                                   textAlign: TextAlign.left, | ||||
|                                   _playback.durationCurrent.value | ||||
|                                       .toHumanReadableString(), | ||||
|                                   style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                                 ), | ||||
|                                 Text( | ||||
|                                   _playback.state.value.activeTrack?.artists | ||||
|                                           ?.asString() ?? | ||||
|                                       'No author', | ||||
|                                   style: Theme.of(context).textTheme.bodyMedium, | ||||
|                                   overflow: TextOverflow.ellipsis, | ||||
|                                   textAlign: TextAlign.left, | ||||
|                                   _playback.durationTotal.value | ||||
|                                       .toHumanReadableString(), | ||||
|                                   style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                           ), | ||||
|                           if (_playback.state.value.activeTrack != null && | ||||
|                               _auth.auth.value != null) | ||||
|                             TrackHeartButton( | ||||
|                               key: ValueKey( | ||||
|                                 _playback.state.value.activeTrack!.id!, | ||||
|                               ), | ||||
|                               trackId: _playback.state.value.activeTrack!.id!, | ||||
|                             ), | ||||
|                         ], | ||||
|                       ).paddingSymmetric(horizontal: 32), | ||||
|                     ), | ||||
|                     const Gap(24), | ||||
|                     Obx( | ||||
|                       () => Column( | ||||
|                             ).paddingSymmetric(horizontal: 8, vertical: 4), | ||||
|                           ], | ||||
|                         ).paddingSymmetric(horizontal: 24), | ||||
|                       ), | ||||
|                       const Gap(24), | ||||
|                       Row( | ||||
|                         mainAxisAlignment: MainAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           SliderTheme( | ||||
|                             data: SliderThemeData( | ||||
|                               trackHeight: 2, | ||||
|                               trackShape: _PlayerProgressTrackShape(), | ||||
|                               thumbShape: const RoundSliderThumbShape( | ||||
|                                 enabledThumbRadius: 8, | ||||
|                               ), | ||||
|                               overlayShape: SliderComponentShape.noOverlay, | ||||
|                             ), | ||||
|                             child: Slider( | ||||
|                               secondaryTrackValue: _playback | ||||
|                                   .durationBuffered.value.inMilliseconds | ||||
|                                   .abs() | ||||
|                                   .toDouble(), | ||||
|                               value: _draggingValue?.abs() ?? | ||||
|                                   _playback.durationCurrent.value.inMilliseconds | ||||
|                                       .toDouble() | ||||
|                                       .abs(), | ||||
|                               min: 0, | ||||
|                               max: max( | ||||
|                                 _playback.durationCurrent.value.inMilliseconds | ||||
|                                     .abs(), | ||||
|                                 _playback.durationTotal.value.inMilliseconds | ||||
|                                     .abs(), | ||||
|                               ).toDouble(), | ||||
|                               onChanged: (value) { | ||||
|                                 setState(() => _draggingValue = value); | ||||
|                               }, | ||||
|                               onChangeEnd: (value) { | ||||
|                                 audioPlayer.seek( | ||||
|                                   Duration(milliseconds: value.toInt()), | ||||
|                                 ); | ||||
|                                 setState(() => _draggingValue = null); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ), | ||||
|                           Row( | ||||
|                             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                             children: [ | ||||
|                               Text( | ||||
|                                 _playback.durationCurrent.value | ||||
|                                     .toHumanReadableString(), | ||||
|                                 style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                               ), | ||||
|                               Text( | ||||
|                                 _playback.durationTotal.value | ||||
|                                     .toHumanReadableString(), | ||||
|                                 style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ).paddingSymmetric(horizontal: 8, vertical: 4), | ||||
|                         ], | ||||
|                       ).paddingSymmetric(horizontal: 24), | ||||
|                     ), | ||||
|                     const Gap(24), | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       children: [ | ||||
|                         StreamBuilder<bool>( | ||||
|                           stream: audioPlayer.shuffledStream, | ||||
|                           builder: (context, snapshot) { | ||||
|                             final shuffled = snapshot.data ?? false; | ||||
|                             return Obx( | ||||
|                               () => IconButton( | ||||
|                                 icon: Icon( | ||||
|                                   shuffled | ||||
|                                       ? Icons.shuffle_on_outlined | ||||
|                                       : Icons.shuffle, | ||||
|                           StreamBuilder<bool>( | ||||
|                             stream: audioPlayer.shuffledStream, | ||||
|                             builder: (context, snapshot) { | ||||
|                               final shuffled = snapshot.data ?? false; | ||||
|                               return Obx( | ||||
|                                 () => IconButton( | ||||
|                                   icon: Icon( | ||||
|                                     shuffled | ||||
|                                         ? Icons.shuffle_on_outlined | ||||
|                                         : Icons.shuffle, | ||||
|                                   ), | ||||
|                                   onPressed: _isFetchingActiveTrack | ||||
|                                       ? null | ||||
|                                       : () { | ||||
|                                           if (shuffled) { | ||||
|                                             audioPlayer.setShuffle(false); | ||||
|                                           } else { | ||||
|                                             audioPlayer.setShuffle(true); | ||||
|                                           } | ||||
|                                         }, | ||||
|                                 ), | ||||
|                                 onPressed: _isFetchingActiveTrack | ||||
|                                     ? null | ||||
|                                     : () { | ||||
|                                         if (shuffled) { | ||||
|                                           audioPlayer.setShuffle(false); | ||||
|                                         } else { | ||||
|                                           audioPlayer.setShuffle(true); | ||||
|                                         } | ||||
|                                       }, | ||||
|                               ), | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                         Obx( | ||||
|                           () => IconButton( | ||||
|                             icon: const Icon(Icons.skip_previous), | ||||
|                             onPressed: _isFetchingActiveTrack | ||||
|                                 ? null | ||||
|                                 : audioPlayer.skipToPrevious, | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Gap(8), | ||||
|                         Obx( | ||||
|                           () => SizedBox( | ||||
|                             width: 56, | ||||
|                             height: 56, | ||||
|                             child: IconButton.filled( | ||||
|                               icon: _isFetchingActiveTrack | ||||
|                                   ? const SizedBox( | ||||
|                                       height: 20, | ||||
|                                       width: 20, | ||||
|                                       child: CircularProgressIndicator( | ||||
|                                         color: Colors.white, | ||||
|                                         strokeWidth: 2.5, | ||||
|                                       ), | ||||
|                                     ) | ||||
|                                   : Icon( | ||||
|                                       !_isPlaying | ||||
|                                           ? Icons.play_arrow | ||||
|                                           : Icons.pause, | ||||
|                                       size: 28, | ||||
|                                     ), | ||||
|                               onPressed: _togglePlayState, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Gap(8), | ||||
|                         Obx( | ||||
|                           () => IconButton( | ||||
|                             icon: const Icon(Icons.skip_next), | ||||
|                             onPressed: _isFetchingActiveTrack | ||||
|                                 ? null | ||||
|                                 : audioPlayer.skipToNext, | ||||
|                           ), | ||||
|                         ), | ||||
|                         Obx( | ||||
|                           () => IconButton( | ||||
|                             icon: Icon( | ||||
|                               _loopMode == PlaylistMode.none | ||||
|                                   ? Icons.repeat | ||||
|                                   : _loopMode == PlaylistMode.loop | ||||
|                                       ? Icons.repeat_on_outlined | ||||
|                                       : Icons.repeat_one_on_outlined, | ||||
|                             ), | ||||
|                             onPressed: _isFetchingActiveTrack | ||||
|                                 ? null | ||||
|                                 : () async { | ||||
|                                     await audioPlayer.setLoopMode( | ||||
|                                       switch (_loopMode) { | ||||
|                                         PlaylistMode.loop => | ||||
|                                           PlaylistMode.single, | ||||
|                                         PlaylistMode.single => | ||||
|                                           PlaylistMode.none, | ||||
|                                         PlaylistMode.none => PlaylistMode.loop, | ||||
|                                       }, | ||||
|                                     ); | ||||
|                                   }, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     const Gap(20), | ||||
|                     Row( | ||||
|                       children: [ | ||||
|                         Expanded( | ||||
|                           child: TextButton.icon( | ||||
|                             icon: const Icon(Icons.queue_music), | ||||
|                             label: const Text('Queue'), | ||||
|                             onPressed: () { | ||||
|                               showModalBottomSheet( | ||||
|                                 useRootNavigator: true, | ||||
|                                 isScrollControlled: true, | ||||
|                                 context: context, | ||||
|                                 builder: (context) => const PlayerQueuePopup(), | ||||
|                               ).then((_) { | ||||
|                                 if (mounted) { | ||||
|                                   setState(() {}); | ||||
|                                 } | ||||
|                               }); | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
|                         ), | ||||
|                         if (!isLargeScreen) const Gap(4), | ||||
|                         if (!isLargeScreen) | ||||
|                           Obx( | ||||
|                             () => IconButton( | ||||
|                               icon: const Icon(Icons.skip_previous), | ||||
|                               onPressed: _isFetchingActiveTrack | ||||
|                                   ? null | ||||
|                                   : audioPlayer.skipToPrevious, | ||||
|                             ), | ||||
|                           ), | ||||
|                           const Gap(8), | ||||
|                           Obx( | ||||
|                             () => SizedBox( | ||||
|                               width: 56, | ||||
|                               height: 56, | ||||
|                               child: IconButton.filled( | ||||
|                                 icon: _isFetchingActiveTrack | ||||
|                                     ? const SizedBox( | ||||
|                                         height: 20, | ||||
|                                         width: 20, | ||||
|                                         child: CircularProgressIndicator( | ||||
|                                           color: Colors.white, | ||||
|                                           strokeWidth: 2.5, | ||||
|                                         ), | ||||
|                                       ) | ||||
|                                     : Icon( | ||||
|                                         !_isPlaying | ||||
|                                             ? Icons.play_arrow | ||||
|                                             : Icons.pause, | ||||
|                                         size: 28, | ||||
|                                       ), | ||||
|                                 onPressed: _togglePlayState, | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                           const Gap(8), | ||||
|                           Obx( | ||||
|                             () => IconButton( | ||||
|                               icon: const Icon(Icons.skip_next), | ||||
|                               onPressed: _isFetchingActiveTrack | ||||
|                                   ? null | ||||
|                                   : audioPlayer.skipToNext, | ||||
|                             ), | ||||
|                           ), | ||||
|                           Obx( | ||||
|                             () => IconButton( | ||||
|                               icon: Icon( | ||||
|                                 _loopMode == PlaylistMode.none | ||||
|                                     ? Icons.repeat | ||||
|                                     : _loopMode == PlaylistMode.loop | ||||
|                                         ? Icons.repeat_on_outlined | ||||
|                                         : Icons.repeat_one_on_outlined, | ||||
|                               ), | ||||
|                               onPressed: _isFetchingActiveTrack | ||||
|                                   ? null | ||||
|                                   : () async { | ||||
|                                       await audioPlayer.setLoopMode( | ||||
|                                         switch (_loopMode) { | ||||
|                                           PlaylistMode.loop => | ||||
|                                             PlaylistMode.single, | ||||
|                                           PlaylistMode.single => | ||||
|                                             PlaylistMode.none, | ||||
|                                           PlaylistMode.none => | ||||
|                                             PlaylistMode.loop, | ||||
|                                         }, | ||||
|                                       ); | ||||
|                                     }, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                       const Gap(20), | ||||
|                       Row( | ||||
|                         children: [ | ||||
|                           Expanded( | ||||
|                             child: TextButton.icon( | ||||
|                               icon: const Icon(Icons.lyrics), | ||||
|                               label: const Text('Lyrics'), | ||||
|                               icon: const Icon(Icons.queue_music), | ||||
|                               label: const Text('Queue'), | ||||
|                               onPressed: () { | ||||
|                                 GoRouter.of(context).pushNamed('playerLyrics'); | ||||
|                                 showModalBottomSheet( | ||||
|                                   useRootNavigator: true, | ||||
|                                   isScrollControlled: true, | ||||
|                                   context: context, | ||||
|                                   builder: (context) => | ||||
|                                       const PlayerQueuePopup(), | ||||
|                                 ).then((_) { | ||||
|                                   if (mounted) { | ||||
|                                     setState(() {}); | ||||
|                                   } | ||||
|                                 }); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ), | ||||
|                         const Gap(4), | ||||
|                         Expanded( | ||||
|                           child: TextButton.icon( | ||||
|                             icon: const Icon(Icons.merge), | ||||
|                             label: const Text('Sources'), | ||||
|                             onPressed: () { | ||||
|                               showModalBottomSheet( | ||||
|                                 useRootNavigator: true, | ||||
|                                 isScrollControlled: true, | ||||
|                                 context: context, | ||||
|                                 builder: (context) => | ||||
|                                     const SiblingTracksPopup(), | ||||
|                               ).then((_) { | ||||
|                                 if (mounted) { | ||||
|                                   setState(() {}); | ||||
|                                 } | ||||
|                               }); | ||||
|                             }, | ||||
|                           if (!isLargeScreen) const Gap(4), | ||||
|                           if (!isLargeScreen) | ||||
|                             Expanded( | ||||
|                               child: TextButton.icon( | ||||
|                                 icon: const Icon(Icons.lyrics), | ||||
|                                 label: const Text('Lyrics'), | ||||
|                                 onPressed: () { | ||||
|                                   GoRouter.of(context) | ||||
|                                       .pushNamed('playerLyrics'); | ||||
|                                 }, | ||||
|                               ), | ||||
|                             ), | ||||
|                           const Gap(4), | ||||
|                           Expanded( | ||||
|                             child: TextButton.icon( | ||||
|                               icon: const Icon(Icons.merge), | ||||
|                               label: const Text('Sources'), | ||||
|                               onPressed: () { | ||||
|                                 showModalBottomSheet( | ||||
|                                   useRootNavigator: true, | ||||
|                                   isScrollControlled: true, | ||||
|                                   context: context, | ||||
|                                   builder: (context) => | ||||
|                                       const SiblingTracksPopup(), | ||||
|                                 ).then((_) { | ||||
|                                   if (mounted) { | ||||
|                                     setState(() {}); | ||||
|                                   } | ||||
|                                 }); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ], | ||||
|                         ], | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               if (isLargeScreen) const Gap(24), | ||||
|               if (isLargeScreen) | ||||
|                 const Expanded( | ||||
|                   child: SyncedLyrics(defaultTextZoom: 67), | ||||
|                 ) | ||||
|             ], | ||||
|                 if (isLargeScreen) const Gap(24), | ||||
|                 if (isLargeScreen) | ||||
|                   const Expanded( | ||||
|                     child: SyncedLyrics(defaultTextZoom: 67), | ||||
|                   ) | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ).marginSymmetric(horizontal: 24), | ||||
|       ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| @@ -5,6 +6,7 @@ import 'package:rhythm_box/providers/auth.dart'; | ||||
| import 'package:rhythm_box/providers/spotify.dart'; | ||||
| import 'package:rhythm_box/providers/user_preferences.dart'; | ||||
| import 'package:rhythm_box/screens/auth/login.dart'; | ||||
| import 'package:rhythm_box/services/database/database.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:rhythm_box/widgets/sized_container.dart'; | ||||
|  | ||||
| @@ -101,6 +103,53 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                 ); | ||||
|               }), | ||||
|               const Divider(thickness: 0.3, height: 1), | ||||
|               Obx( | ||||
|                 () => ListTile( | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                   leading: const Icon(Icons.audio_file), | ||||
|                   title: const Text('Audio Source'), | ||||
|                   subtitle: | ||||
|                       const Text('Choose who to provide the songs you played.'), | ||||
|                   trailing: DropdownButtonHideUnderline( | ||||
|                     child: DropdownButton2<AudioSource>( | ||||
|                       isExpanded: true, | ||||
|                       hint: Text( | ||||
|                         'Select Item', | ||||
|                         style: TextStyle( | ||||
|                           fontSize: 14, | ||||
|                           color: Theme.of(context).hintColor, | ||||
|                         ), | ||||
|                       ), | ||||
|                       items: AudioSource.values | ||||
|                           .map((AudioSource item) => | ||||
|                               DropdownMenuItem<AudioSource>( | ||||
|                                 value: item, | ||||
|                                 child: Text( | ||||
|                                   item.label, | ||||
|                                   style: const TextStyle( | ||||
|                                     fontSize: 14, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               )) | ||||
|                           .toList(), | ||||
|                       value: _preferences.state.value.audioSource, | ||||
|                       onChanged: (AudioSource? value) { | ||||
|                         _preferences | ||||
|                             .setAudioSource(value ?? AudioSource.youtube); | ||||
|                       }, | ||||
|                       buttonStyleData: const ButtonStyleData( | ||||
|                         padding: EdgeInsets.symmetric(horizontal: 16), | ||||
|                         height: 40, | ||||
|                         width: 140, | ||||
|                       ), | ||||
|                       menuItemStyleData: const MenuItemStyleData( | ||||
|                         height: 40, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               const Divider(thickness: 0.3, height: 1), | ||||
|               Obx( | ||||
|                 () => SwitchListTile( | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|   | ||||
| @@ -94,7 +94,7 @@ abstract class AudioPlayerInterface { | ||||
|           ), | ||||
|         ) { | ||||
|     _mkPlayer.stream.error.listen((event) { | ||||
|       Get.find<ErrorNotifier>().logError('[Playback] Error: $event'); | ||||
|       Get.find<ErrorNotifier>().logError('[Playback][Player] Error: $event'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -50,7 +50,8 @@ class CustomPlayer extends Player { | ||||
|         } | ||||
|       }), | ||||
|       stream.error.listen((event) { | ||||
|         Get.find<ErrorNotifier>().logError('[Playback] Error: $event'); | ||||
|         Get.find<ErrorNotifier>() | ||||
|             .logError('[Playback][CustomLayer] Error: $event'); | ||||
|       }), | ||||
|     ]; | ||||
|     PackageInfo.fromPlatform().then((packageInfo) { | ||||
|   | ||||
| @@ -453,6 +453,15 @@ class $PreferencesTableTable extends PreferencesTable | ||||
|       type: DriftSqlType.string, | ||||
|       requiredDuringInsert: false, | ||||
|       defaultValue: const Constant('https://pipedapi.kavin.rocks')); | ||||
|   static const VerificationMeta _neteaseApiInstanceMeta = | ||||
|       const VerificationMeta('neteaseApiInstance'); | ||||
|   @override | ||||
|   late final GeneratedColumn<String> neteaseApiInstance = | ||||
|       GeneratedColumn<String>('netease_api_instance', aliasedName, false, | ||||
|           type: DriftSqlType.string, | ||||
|           requiredDuringInsert: false, | ||||
|           defaultValue: | ||||
|               const Constant('https://rhythmbox-netease-music-api.vercel.app')); | ||||
|   static const VerificationMeta _themeModeMeta = | ||||
|       const VerificationMeta('themeMode'); | ||||
|   @override | ||||
| @@ -494,16 +503,6 @@ class $PreferencesTableTable extends PreferencesTable | ||||
|               defaultValue: Constant(SourceCodecs.m4a.name)) | ||||
|           .withConverter<SourceCodecs>( | ||||
|               $PreferencesTableTable.$converterdownloadMusicCodec); | ||||
|   static const VerificationMeta _discordPresenceMeta = | ||||
|       const VerificationMeta('discordPresence'); | ||||
|   @override | ||||
|   late final GeneratedColumn<bool> discordPresence = GeneratedColumn<bool>( | ||||
|       'discord_presence', aliasedName, false, | ||||
|       type: DriftSqlType.bool, | ||||
|       requiredDuringInsert: false, | ||||
|       defaultConstraints: GeneratedColumn.constraintIsAlways( | ||||
|           'CHECK ("discord_presence" IN (0, 1))'), | ||||
|       defaultValue: const Constant(true)); | ||||
|   static const VerificationMeta _endlessPlaybackMeta = | ||||
|       const VerificationMeta('endlessPlayback'); | ||||
|   @override | ||||
| @@ -543,11 +542,11 @@ class $PreferencesTableTable extends PreferencesTable | ||||
|         downloadLocation, | ||||
|         localLibraryLocation, | ||||
|         pipedInstance, | ||||
|         neteaseApiInstance, | ||||
|         themeMode, | ||||
|         audioSource, | ||||
|         streamMusicCodec, | ||||
|         downloadMusicCodec, | ||||
|         discordPresence, | ||||
|         endlessPlayback, | ||||
|         playerWakelock | ||||
|       ]; | ||||
| @@ -622,16 +621,16 @@ class $PreferencesTableTable extends PreferencesTable | ||||
|           pipedInstance.isAcceptableOrUnknown( | ||||
|               data['piped_instance']!, _pipedInstanceMeta)); | ||||
|     } | ||||
|     if (data.containsKey('netease_api_instance')) { | ||||
|       context.handle( | ||||
|           _neteaseApiInstanceMeta, | ||||
|           neteaseApiInstance.isAcceptableOrUnknown( | ||||
|               data['netease_api_instance']!, _neteaseApiInstanceMeta)); | ||||
|     } | ||||
|     context.handle(_themeModeMeta, const VerificationResult.success()); | ||||
|     context.handle(_audioSourceMeta, const VerificationResult.success()); | ||||
|     context.handle(_streamMusicCodecMeta, const VerificationResult.success()); | ||||
|     context.handle(_downloadMusicCodecMeta, const VerificationResult.success()); | ||||
|     if (data.containsKey('discord_presence')) { | ||||
|       context.handle( | ||||
|           _discordPresenceMeta, | ||||
|           discordPresence.isAcceptableOrUnknown( | ||||
|               data['discord_presence']!, _discordPresenceMeta)); | ||||
|     } | ||||
|     if (data.containsKey('endless_playback')) { | ||||
|       context.handle( | ||||
|           _endlessPlaybackMeta, | ||||
| @@ -696,6 +695,8 @@ class $PreferencesTableTable extends PreferencesTable | ||||
|               data['${effectivePrefix}local_library_location'])!), | ||||
|       pipedInstance: attachedDatabase.typeMapping | ||||
|           .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, | ||||
|       neteaseApiInstance: attachedDatabase.typeMapping.read( | ||||
|           DriftSqlType.string, data['${effectivePrefix}netease_api_instance'])!, | ||||
|       themeMode: $PreferencesTableTable.$converterthemeMode.fromSql( | ||||
|           attachedDatabase.typeMapping.read( | ||||
|               DriftSqlType.string, data['${effectivePrefix}theme_mode'])!), | ||||
| @@ -708,8 +709,6 @@ class $PreferencesTableTable extends PreferencesTable | ||||
|       downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec | ||||
|           .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, | ||||
|               data['${effectivePrefix}download_music_codec'])!), | ||||
|       discordPresence: attachedDatabase.typeMapping | ||||
|           .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, | ||||
|       endlessPlayback: attachedDatabase.typeMapping | ||||
|           .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, | ||||
|       playerWakelock: attachedDatabase.typeMapping | ||||
| @@ -771,11 +770,11 @@ class PreferencesTableData extends DataClass | ||||
|   final String downloadLocation; | ||||
|   final List<String> localLibraryLocation; | ||||
|   final String pipedInstance; | ||||
|   final String neteaseApiInstance; | ||||
|   final ThemeMode themeMode; | ||||
|   final AudioSource audioSource; | ||||
|   final SourceCodecs streamMusicCodec; | ||||
|   final SourceCodecs downloadMusicCodec; | ||||
|   final bool discordPresence; | ||||
|   final bool endlessPlayback; | ||||
|   final bool playerWakelock; | ||||
|   const PreferencesTableData( | ||||
| @@ -796,11 +795,11 @@ class PreferencesTableData extends DataClass | ||||
|       required this.downloadLocation, | ||||
|       required this.localLibraryLocation, | ||||
|       required this.pipedInstance, | ||||
|       required this.neteaseApiInstance, | ||||
|       required this.themeMode, | ||||
|       required this.audioSource, | ||||
|       required this.streamMusicCodec, | ||||
|       required this.downloadMusicCodec, | ||||
|       required this.discordPresence, | ||||
|       required this.endlessPlayback, | ||||
|       required this.playerWakelock}); | ||||
|   @override | ||||
| @@ -849,6 +848,7 @@ class PreferencesTableData extends DataClass | ||||
|           .toSql(localLibraryLocation)); | ||||
|     } | ||||
|     map['piped_instance'] = Variable<String>(pipedInstance); | ||||
|     map['netease_api_instance'] = Variable<String>(neteaseApiInstance); | ||||
|     { | ||||
|       map['theme_mode'] = Variable<String>( | ||||
|           $PreferencesTableTable.$converterthemeMode.toSql(themeMode)); | ||||
| @@ -867,7 +867,6 @@ class PreferencesTableData extends DataClass | ||||
|           .$converterdownloadMusicCodec | ||||
|           .toSql(downloadMusicCodec)); | ||||
|     } | ||||
|     map['discord_presence'] = Variable<bool>(discordPresence); | ||||
|     map['endless_playback'] = Variable<bool>(endlessPlayback); | ||||
|     map['player_wakelock'] = Variable<bool>(playerWakelock); | ||||
|     return map; | ||||
| @@ -892,11 +891,11 @@ class PreferencesTableData extends DataClass | ||||
|       downloadLocation: Value(downloadLocation), | ||||
|       localLibraryLocation: Value(localLibraryLocation), | ||||
|       pipedInstance: Value(pipedInstance), | ||||
|       neteaseApiInstance: Value(neteaseApiInstance), | ||||
|       themeMode: Value(themeMode), | ||||
|       audioSource: Value(audioSource), | ||||
|       streamMusicCodec: Value(streamMusicCodec), | ||||
|       downloadMusicCodec: Value(downloadMusicCodec), | ||||
|       discordPresence: Value(discordPresence), | ||||
|       endlessPlayback: Value(endlessPlayback), | ||||
|       playerWakelock: Value(playerWakelock), | ||||
|     ); | ||||
| @@ -930,6 +929,8 @@ class PreferencesTableData extends DataClass | ||||
|       localLibraryLocation: | ||||
|           serializer.fromJson<List<String>>(json['localLibraryLocation']), | ||||
|       pipedInstance: serializer.fromJson<String>(json['pipedInstance']), | ||||
|       neteaseApiInstance: | ||||
|           serializer.fromJson<String>(json['neteaseApiInstance']), | ||||
|       themeMode: $PreferencesTableTable.$converterthemeMode | ||||
|           .fromJson(serializer.fromJson<String>(json['themeMode'])), | ||||
|       audioSource: $PreferencesTableTable.$converteraudioSource | ||||
| @@ -938,7 +939,6 @@ class PreferencesTableData extends DataClass | ||||
|           .fromJson(serializer.fromJson<String>(json['streamMusicCodec'])), | ||||
|       downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec | ||||
|           .fromJson(serializer.fromJson<String>(json['downloadMusicCodec'])), | ||||
|       discordPresence: serializer.fromJson<bool>(json['discordPresence']), | ||||
|       endlessPlayback: serializer.fromJson<bool>(json['endlessPlayback']), | ||||
|       playerWakelock: serializer.fromJson<bool>(json['playerWakelock']), | ||||
|     ); | ||||
| @@ -970,6 +970,7 @@ class PreferencesTableData extends DataClass | ||||
|       'localLibraryLocation': | ||||
|           serializer.toJson<List<String>>(localLibraryLocation), | ||||
|       'pipedInstance': serializer.toJson<String>(pipedInstance), | ||||
|       'neteaseApiInstance': serializer.toJson<String>(neteaseApiInstance), | ||||
|       'themeMode': serializer.toJson<String>( | ||||
|           $PreferencesTableTable.$converterthemeMode.toJson(themeMode)), | ||||
|       'audioSource': serializer.toJson<String>( | ||||
| @@ -980,7 +981,6 @@ class PreferencesTableData extends DataClass | ||||
|       'downloadMusicCodec': serializer.toJson<String>($PreferencesTableTable | ||||
|           .$converterdownloadMusicCodec | ||||
|           .toJson(downloadMusicCodec)), | ||||
|       'discordPresence': serializer.toJson<bool>(discordPresence), | ||||
|       'endlessPlayback': serializer.toJson<bool>(endlessPlayback), | ||||
|       'playerWakelock': serializer.toJson<bool>(playerWakelock), | ||||
|     }; | ||||
| @@ -1004,11 +1004,11 @@ class PreferencesTableData extends DataClass | ||||
|           String? downloadLocation, | ||||
|           List<String>? localLibraryLocation, | ||||
|           String? pipedInstance, | ||||
|           String? neteaseApiInstance, | ||||
|           ThemeMode? themeMode, | ||||
|           AudioSource? audioSource, | ||||
|           SourceCodecs? streamMusicCodec, | ||||
|           SourceCodecs? downloadMusicCodec, | ||||
|           bool? discordPresence, | ||||
|           bool? endlessPlayback, | ||||
|           bool? playerWakelock}) => | ||||
|       PreferencesTableData( | ||||
| @@ -1029,11 +1029,11 @@ class PreferencesTableData extends DataClass | ||||
|         downloadLocation: downloadLocation ?? this.downloadLocation, | ||||
|         localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, | ||||
|         pipedInstance: pipedInstance ?? this.pipedInstance, | ||||
|         neteaseApiInstance: neteaseApiInstance ?? this.neteaseApiInstance, | ||||
|         themeMode: themeMode ?? this.themeMode, | ||||
|         audioSource: audioSource ?? this.audioSource, | ||||
|         streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, | ||||
|         downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, | ||||
|         discordPresence: discordPresence ?? this.discordPresence, | ||||
|         endlessPlayback: endlessPlayback ?? this.endlessPlayback, | ||||
|         playerWakelock: playerWakelock ?? this.playerWakelock, | ||||
|       ); | ||||
| @@ -1081,6 +1081,9 @@ class PreferencesTableData extends DataClass | ||||
|       pipedInstance: data.pipedInstance.present | ||||
|           ? data.pipedInstance.value | ||||
|           : this.pipedInstance, | ||||
|       neteaseApiInstance: data.neteaseApiInstance.present | ||||
|           ? data.neteaseApiInstance.value | ||||
|           : this.neteaseApiInstance, | ||||
|       themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, | ||||
|       audioSource: | ||||
|           data.audioSource.present ? data.audioSource.value : this.audioSource, | ||||
| @@ -1090,9 +1093,6 @@ class PreferencesTableData extends DataClass | ||||
|       downloadMusicCodec: data.downloadMusicCodec.present | ||||
|           ? data.downloadMusicCodec.value | ||||
|           : this.downloadMusicCodec, | ||||
|       discordPresence: data.discordPresence.present | ||||
|           ? data.discordPresence.value | ||||
|           : this.discordPresence, | ||||
|       endlessPlayback: data.endlessPlayback.present | ||||
|           ? data.endlessPlayback.value | ||||
|           : this.endlessPlayback, | ||||
| @@ -1122,11 +1122,11 @@ class PreferencesTableData extends DataClass | ||||
|           ..write('downloadLocation: $downloadLocation, ') | ||||
|           ..write('localLibraryLocation: $localLibraryLocation, ') | ||||
|           ..write('pipedInstance: $pipedInstance, ') | ||||
|           ..write('neteaseApiInstance: $neteaseApiInstance, ') | ||||
|           ..write('themeMode: $themeMode, ') | ||||
|           ..write('audioSource: $audioSource, ') | ||||
|           ..write('streamMusicCodec: $streamMusicCodec, ') | ||||
|           ..write('downloadMusicCodec: $downloadMusicCodec, ') | ||||
|           ..write('discordPresence: $discordPresence, ') | ||||
|           ..write('endlessPlayback: $endlessPlayback, ') | ||||
|           ..write('playerWakelock: $playerWakelock') | ||||
|           ..write(')')) | ||||
| @@ -1152,11 +1152,11 @@ class PreferencesTableData extends DataClass | ||||
|         downloadLocation, | ||||
|         localLibraryLocation, | ||||
|         pipedInstance, | ||||
|         neteaseApiInstance, | ||||
|         themeMode, | ||||
|         audioSource, | ||||
|         streamMusicCodec, | ||||
|         downloadMusicCodec, | ||||
|         discordPresence, | ||||
|         endlessPlayback, | ||||
|         playerWakelock | ||||
|       ]); | ||||
| @@ -1181,11 +1181,11 @@ class PreferencesTableData extends DataClass | ||||
|           other.downloadLocation == this.downloadLocation && | ||||
|           other.localLibraryLocation == this.localLibraryLocation && | ||||
|           other.pipedInstance == this.pipedInstance && | ||||
|           other.neteaseApiInstance == this.neteaseApiInstance && | ||||
|           other.themeMode == this.themeMode && | ||||
|           other.audioSource == this.audioSource && | ||||
|           other.streamMusicCodec == this.streamMusicCodec && | ||||
|           other.downloadMusicCodec == this.downloadMusicCodec && | ||||
|           other.discordPresence == this.discordPresence && | ||||
|           other.endlessPlayback == this.endlessPlayback && | ||||
|           other.playerWakelock == this.playerWakelock); | ||||
| } | ||||
| @@ -1208,11 +1208,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|   final Value<String> downloadLocation; | ||||
|   final Value<List<String>> localLibraryLocation; | ||||
|   final Value<String> pipedInstance; | ||||
|   final Value<String> neteaseApiInstance; | ||||
|   final Value<ThemeMode> themeMode; | ||||
|   final Value<AudioSource> audioSource; | ||||
|   final Value<SourceCodecs> streamMusicCodec; | ||||
|   final Value<SourceCodecs> downloadMusicCodec; | ||||
|   final Value<bool> discordPresence; | ||||
|   final Value<bool> endlessPlayback; | ||||
|   final Value<bool> playerWakelock; | ||||
|   const PreferencesTableCompanion({ | ||||
| @@ -1233,11 +1233,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|     this.downloadLocation = const Value.absent(), | ||||
|     this.localLibraryLocation = const Value.absent(), | ||||
|     this.pipedInstance = const Value.absent(), | ||||
|     this.neteaseApiInstance = const Value.absent(), | ||||
|     this.themeMode = const Value.absent(), | ||||
|     this.audioSource = const Value.absent(), | ||||
|     this.streamMusicCodec = const Value.absent(), | ||||
|     this.downloadMusicCodec = const Value.absent(), | ||||
|     this.discordPresence = const Value.absent(), | ||||
|     this.endlessPlayback = const Value.absent(), | ||||
|     this.playerWakelock = const Value.absent(), | ||||
|   }); | ||||
| @@ -1259,11 +1259,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|     this.downloadLocation = const Value.absent(), | ||||
|     this.localLibraryLocation = const Value.absent(), | ||||
|     this.pipedInstance = const Value.absent(), | ||||
|     this.neteaseApiInstance = const Value.absent(), | ||||
|     this.themeMode = const Value.absent(), | ||||
|     this.audioSource = const Value.absent(), | ||||
|     this.streamMusicCodec = const Value.absent(), | ||||
|     this.downloadMusicCodec = const Value.absent(), | ||||
|     this.discordPresence = const Value.absent(), | ||||
|     this.endlessPlayback = const Value.absent(), | ||||
|     this.playerWakelock = const Value.absent(), | ||||
|   }); | ||||
| @@ -1285,11 +1285,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|     Expression<String>? downloadLocation, | ||||
|     Expression<String>? localLibraryLocation, | ||||
|     Expression<String>? pipedInstance, | ||||
|     Expression<String>? neteaseApiInstance, | ||||
|     Expression<String>? themeMode, | ||||
|     Expression<String>? audioSource, | ||||
|     Expression<String>? streamMusicCodec, | ||||
|     Expression<String>? downloadMusicCodec, | ||||
|     Expression<bool>? discordPresence, | ||||
|     Expression<bool>? endlessPlayback, | ||||
|     Expression<bool>? playerWakelock, | ||||
|   }) { | ||||
| @@ -1313,12 +1313,13 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|       if (localLibraryLocation != null) | ||||
|         'local_library_location': localLibraryLocation, | ||||
|       if (pipedInstance != null) 'piped_instance': pipedInstance, | ||||
|       if (neteaseApiInstance != null) | ||||
|         'netease_api_instance': neteaseApiInstance, | ||||
|       if (themeMode != null) 'theme_mode': themeMode, | ||||
|       if (audioSource != null) 'audio_source': audioSource, | ||||
|       if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, | ||||
|       if (downloadMusicCodec != null) | ||||
|         'download_music_codec': downloadMusicCodec, | ||||
|       if (discordPresence != null) 'discord_presence': discordPresence, | ||||
|       if (endlessPlayback != null) 'endless_playback': endlessPlayback, | ||||
|       if (playerWakelock != null) 'player_wakelock': playerWakelock, | ||||
|     }); | ||||
| @@ -1342,11 +1343,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|       Value<String>? downloadLocation, | ||||
|       Value<List<String>>? localLibraryLocation, | ||||
|       Value<String>? pipedInstance, | ||||
|       Value<String>? neteaseApiInstance, | ||||
|       Value<ThemeMode>? themeMode, | ||||
|       Value<AudioSource>? audioSource, | ||||
|       Value<SourceCodecs>? streamMusicCodec, | ||||
|       Value<SourceCodecs>? downloadMusicCodec, | ||||
|       Value<bool>? discordPresence, | ||||
|       Value<bool>? endlessPlayback, | ||||
|       Value<bool>? playerWakelock}) { | ||||
|     return PreferencesTableCompanion( | ||||
| @@ -1367,11 +1368,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|       downloadLocation: downloadLocation ?? this.downloadLocation, | ||||
|       localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, | ||||
|       pipedInstance: pipedInstance ?? this.pipedInstance, | ||||
|       neteaseApiInstance: neteaseApiInstance ?? this.neteaseApiInstance, | ||||
|       themeMode: themeMode ?? this.themeMode, | ||||
|       audioSource: audioSource ?? this.audioSource, | ||||
|       streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, | ||||
|       downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, | ||||
|       discordPresence: discordPresence ?? this.discordPresence, | ||||
|       endlessPlayback: endlessPlayback ?? this.endlessPlayback, | ||||
|       playerWakelock: playerWakelock ?? this.playerWakelock, | ||||
|     ); | ||||
| @@ -1443,6 +1444,9 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|     if (pipedInstance.present) { | ||||
|       map['piped_instance'] = Variable<String>(pipedInstance.value); | ||||
|     } | ||||
|     if (neteaseApiInstance.present) { | ||||
|       map['netease_api_instance'] = Variable<String>(neteaseApiInstance.value); | ||||
|     } | ||||
|     if (themeMode.present) { | ||||
|       map['theme_mode'] = Variable<String>( | ||||
|           $PreferencesTableTable.$converterthemeMode.toSql(themeMode.value)); | ||||
| @@ -1462,9 +1466,6 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|           .$converterdownloadMusicCodec | ||||
|           .toSql(downloadMusicCodec.value)); | ||||
|     } | ||||
|     if (discordPresence.present) { | ||||
|       map['discord_presence'] = Variable<bool>(discordPresence.value); | ||||
|     } | ||||
|     if (endlessPlayback.present) { | ||||
|       map['endless_playback'] = Variable<bool>(endlessPlayback.value); | ||||
|     } | ||||
| @@ -1494,11 +1495,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> { | ||||
|           ..write('downloadLocation: $downloadLocation, ') | ||||
|           ..write('localLibraryLocation: $localLibraryLocation, ') | ||||
|           ..write('pipedInstance: $pipedInstance, ') | ||||
|           ..write('neteaseApiInstance: $neteaseApiInstance, ') | ||||
|           ..write('themeMode: $themeMode, ') | ||||
|           ..write('audioSource: $audioSource, ') | ||||
|           ..write('streamMusicCodec: $streamMusicCodec, ') | ||||
|           ..write('downloadMusicCodec: $downloadMusicCodec, ') | ||||
|           ..write('discordPresence: $discordPresence, ') | ||||
|           ..write('endlessPlayback: $endlessPlayback, ') | ||||
|           ..write('playerWakelock: $playerWakelock') | ||||
|           ..write(')')) | ||||
| @@ -3950,11 +3951,11 @@ typedef $$PreferencesTableTableCreateCompanionBuilder | ||||
|   Value<String> downloadLocation, | ||||
|   Value<List<String>> localLibraryLocation, | ||||
|   Value<String> pipedInstance, | ||||
|   Value<String> neteaseApiInstance, | ||||
|   Value<ThemeMode> themeMode, | ||||
|   Value<AudioSource> audioSource, | ||||
|   Value<SourceCodecs> streamMusicCodec, | ||||
|   Value<SourceCodecs> downloadMusicCodec, | ||||
|   Value<bool> discordPresence, | ||||
|   Value<bool> endlessPlayback, | ||||
|   Value<bool> playerWakelock, | ||||
| }); | ||||
| @@ -3977,11 +3978,11 @@ typedef $$PreferencesTableTableUpdateCompanionBuilder | ||||
|   Value<String> downloadLocation, | ||||
|   Value<List<String>> localLibraryLocation, | ||||
|   Value<String> pipedInstance, | ||||
|   Value<String> neteaseApiInstance, | ||||
|   Value<ThemeMode> themeMode, | ||||
|   Value<AudioSource> audioSource, | ||||
|   Value<SourceCodecs> streamMusicCodec, | ||||
|   Value<SourceCodecs> downloadMusicCodec, | ||||
|   Value<bool> discordPresence, | ||||
|   Value<bool> endlessPlayback, | ||||
|   Value<bool> playerWakelock, | ||||
| }); | ||||
| @@ -4021,11 +4022,11 @@ class $$PreferencesTableTableTableManager extends RootTableManager< | ||||
|             Value<String> downloadLocation = const Value.absent(), | ||||
|             Value<List<String>> localLibraryLocation = const Value.absent(), | ||||
|             Value<String> pipedInstance = const Value.absent(), | ||||
|             Value<String> neteaseApiInstance = const Value.absent(), | ||||
|             Value<ThemeMode> themeMode = const Value.absent(), | ||||
|             Value<AudioSource> audioSource = const Value.absent(), | ||||
|             Value<SourceCodecs> streamMusicCodec = const Value.absent(), | ||||
|             Value<SourceCodecs> downloadMusicCodec = const Value.absent(), | ||||
|             Value<bool> discordPresence = const Value.absent(), | ||||
|             Value<bool> endlessPlayback = const Value.absent(), | ||||
|             Value<bool> playerWakelock = const Value.absent(), | ||||
|           }) => | ||||
| @@ -4047,11 +4048,11 @@ class $$PreferencesTableTableTableManager extends RootTableManager< | ||||
|             downloadLocation: downloadLocation, | ||||
|             localLibraryLocation: localLibraryLocation, | ||||
|             pipedInstance: pipedInstance, | ||||
|             neteaseApiInstance: neteaseApiInstance, | ||||
|             themeMode: themeMode, | ||||
|             audioSource: audioSource, | ||||
|             streamMusicCodec: streamMusicCodec, | ||||
|             downloadMusicCodec: downloadMusicCodec, | ||||
|             discordPresence: discordPresence, | ||||
|             endlessPlayback: endlessPlayback, | ||||
|             playerWakelock: playerWakelock, | ||||
|           ), | ||||
| @@ -4073,11 +4074,11 @@ class $$PreferencesTableTableTableManager extends RootTableManager< | ||||
|             Value<String> downloadLocation = const Value.absent(), | ||||
|             Value<List<String>> localLibraryLocation = const Value.absent(), | ||||
|             Value<String> pipedInstance = const Value.absent(), | ||||
|             Value<String> neteaseApiInstance = const Value.absent(), | ||||
|             Value<ThemeMode> themeMode = const Value.absent(), | ||||
|             Value<AudioSource> audioSource = const Value.absent(), | ||||
|             Value<SourceCodecs> streamMusicCodec = const Value.absent(), | ||||
|             Value<SourceCodecs> downloadMusicCodec = const Value.absent(), | ||||
|             Value<bool> discordPresence = const Value.absent(), | ||||
|             Value<bool> endlessPlayback = const Value.absent(), | ||||
|             Value<bool> playerWakelock = const Value.absent(), | ||||
|           }) => | ||||
| @@ -4099,11 +4100,11 @@ class $$PreferencesTableTableTableManager extends RootTableManager< | ||||
|             downloadLocation: downloadLocation, | ||||
|             localLibraryLocation: localLibraryLocation, | ||||
|             pipedInstance: pipedInstance, | ||||
|             neteaseApiInstance: neteaseApiInstance, | ||||
|             themeMode: themeMode, | ||||
|             audioSource: audioSource, | ||||
|             streamMusicCodec: streamMusicCodec, | ||||
|             downloadMusicCodec: downloadMusicCodec, | ||||
|             discordPresence: discordPresence, | ||||
|             endlessPlayback: endlessPlayback, | ||||
|             playerWakelock: playerWakelock, | ||||
|           ), | ||||
| @@ -4214,6 +4215,11 @@ class $$PreferencesTableTableFilterComposer | ||||
|       builder: (column, joinBuilders) => | ||||
|           ColumnFilters(column, joinBuilders: joinBuilders)); | ||||
|  | ||||
|   ColumnFilters<String> get neteaseApiInstance => $state.composableBuilder( | ||||
|       column: $state.table.neteaseApiInstance, | ||||
|       builder: (column, joinBuilders) => | ||||
|           ColumnFilters(column, joinBuilders: joinBuilders)); | ||||
|  | ||||
|   ColumnWithTypeConverterFilters<ThemeMode, ThemeMode, String> get themeMode => | ||||
|       $state.composableBuilder( | ||||
|           column: $state.table.themeMode, | ||||
| @@ -4242,11 +4248,6 @@ class $$PreferencesTableTableFilterComposer | ||||
|               column, | ||||
|               joinBuilders: joinBuilders)); | ||||
|  | ||||
|   ColumnFilters<bool> get discordPresence => $state.composableBuilder( | ||||
|       column: $state.table.discordPresence, | ||||
|       builder: (column, joinBuilders) => | ||||
|           ColumnFilters(column, joinBuilders: joinBuilders)); | ||||
|  | ||||
|   ColumnFilters<bool> get endlessPlayback => $state.composableBuilder( | ||||
|       column: $state.table.endlessPlayback, | ||||
|       builder: (column, joinBuilders) => | ||||
| @@ -4346,6 +4347,11 @@ class $$PreferencesTableTableOrderingComposer | ||||
|       builder: (column, joinBuilders) => | ||||
|           ColumnOrderings(column, joinBuilders: joinBuilders)); | ||||
|  | ||||
|   ColumnOrderings<String> get neteaseApiInstance => $state.composableBuilder( | ||||
|       column: $state.table.neteaseApiInstance, | ||||
|       builder: (column, joinBuilders) => | ||||
|           ColumnOrderings(column, joinBuilders: joinBuilders)); | ||||
|  | ||||
|   ColumnOrderings<String> get themeMode => $state.composableBuilder( | ||||
|       column: $state.table.themeMode, | ||||
|       builder: (column, joinBuilders) => | ||||
| @@ -4366,11 +4372,6 @@ class $$PreferencesTableTableOrderingComposer | ||||
|       builder: (column, joinBuilders) => | ||||
|           ColumnOrderings(column, joinBuilders: joinBuilders)); | ||||
|  | ||||
|   ColumnOrderings<bool> get discordPresence => $state.composableBuilder( | ||||
|       column: $state.table.discordPresence, | ||||
|       builder: (column, joinBuilders) => | ||||
|           ColumnOrderings(column, joinBuilders: joinBuilders)); | ||||
|  | ||||
|   ColumnOrderings<bool> get endlessPlayback => $state.composableBuilder( | ||||
|       column: $state.table.endlessPlayback, | ||||
|       builder: (column, joinBuilders) => | ||||
|   | ||||
| @@ -13,7 +13,8 @@ enum CloseBehavior { | ||||
|  | ||||
| enum AudioSource { | ||||
|   youtube, | ||||
|   piped; | ||||
|   piped, | ||||
|   netease; | ||||
|  | ||||
|   String get label => name[0].toUpperCase() + name.substring(1); | ||||
| } | ||||
| @@ -74,6 +75,8 @@ class PreferencesTable extends Table { | ||||
|       text().withDefault(const Constant('')).map(const StringListConverter())(); | ||||
|   TextColumn get pipedInstance => | ||||
|       text().withDefault(const Constant('https://pipedapi.kavin.rocks'))(); | ||||
|   TextColumn get neteaseApiInstance => text().withDefault( | ||||
|       const Constant('https://rhythmbox-netease-music-api.vercel.app'))(); | ||||
|   TextColumn get themeMode => | ||||
|       textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))(); | ||||
|   TextColumn get audioSource => | ||||
| @@ -82,8 +85,6 @@ class PreferencesTable extends Table { | ||||
|       textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.weba.name))(); | ||||
|   TextColumn get downloadMusicCodec => | ||||
|       textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.m4a.name))(); | ||||
|   BoolColumn get discordPresence => | ||||
|       boolean().withDefault(const Constant(true))(); | ||||
|   BoolColumn get endlessPlayback => | ||||
|       boolean().withDefault(const Constant(true))(); | ||||
|   BoolColumn get playerWakelock => | ||||
| @@ -108,12 +109,12 @@ class PreferencesTable extends Table { | ||||
|       searchMode: SearchMode.youtube, | ||||
|       downloadLocation: '', | ||||
|       localLibraryLocation: [], | ||||
|       neteaseApiInstance: 'https://rhythmbox-netease-music-api.vercel.app', | ||||
|       pipedInstance: 'https://pipedapi.kavin.rocks', | ||||
|       themeMode: ThemeMode.system, | ||||
|       audioSource: AudioSource.youtube, | ||||
|       streamMusicCodec: SourceCodecs.weba, | ||||
|       downloadMusicCodec: SourceCodecs.m4a, | ||||
|       discordPresence: true, | ||||
|       endlessPlayback: true, | ||||
|       playerWakelock: true, | ||||
|     ); | ||||
|   | ||||
| @@ -2,7 +2,8 @@ part of '../database.dart'; | ||||
|  | ||||
| enum SourceType { | ||||
|   youtube._('YouTube'), | ||||
|   youtubeMusic._('YouTube Music'); | ||||
|   youtubeMusic._('YouTube Music'), | ||||
|   netease._('Netease Music'); | ||||
|  | ||||
|   final String label; | ||||
|  | ||||
|   | ||||
| @@ -86,7 +86,7 @@ abstract class AudioPlayerInterface { | ||||
|           ), | ||||
|         ) { | ||||
|     _mkPlayer.stream.error.listen((event) { | ||||
|       Get.find<ErrorNotifier>().logError('[Playback] Error: $event'); | ||||
|       Get.find<ErrorNotifier>().logError('[Playback][Media] Error: $event'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import 'package:rhythm_box/providers/error_notifier.dart'; | ||||
| import 'package:rhythm_box/services/audio_player/audio_player.dart'; | ||||
| import 'package:rhythm_box/services/server/active_sourced_track.dart'; | ||||
| import 'package:rhythm_box/services/server/sourced_track.dart'; | ||||
| import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; | ||||
| import 'package:shelf/shelf.dart'; | ||||
|  | ||||
| class ServerPlaybackRoutesProvider { | ||||
| @@ -24,14 +25,25 @@ class ServerPlaybackRoutesProvider { | ||||
|  | ||||
|       activeSourcedTrack.updateTrack(sourcedTrack); | ||||
|  | ||||
|       var url = sourcedTrack!.url; | ||||
|  | ||||
|       if (sourcedTrack is NeteaseSourcedTrack) { | ||||
|         // Special processing for netease to get real assets url | ||||
|         final resp = await GetConnect(timeout: const Duration(seconds: 30)).get( | ||||
|           '${sourcedTrack.url}&realIP=${await NeteaseSourcedTrack.lookupRealIp()}', | ||||
|         ); | ||||
|         final realUrl = resp.body['data'][0]['url']; | ||||
|         url = realUrl; | ||||
|       } | ||||
|  | ||||
|       final res = await Dio().get( | ||||
|         sourcedTrack!.url, | ||||
|         url, | ||||
|         options: Options( | ||||
|           headers: { | ||||
|             ...request.headers, | ||||
|             'User-Agent': | ||||
|                 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', | ||||
|             'host': Uri.parse(sourcedTrack.url).host, | ||||
|             'host': Uri.parse(url).host, | ||||
|             'Cache-Control': 'max-age=0', | ||||
|             'Connection': 'keep-alive', | ||||
|           }, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:rhythm_box/providers/user_preferences.dart'; | ||||
| import 'package:rhythm_box/services/database/database.dart'; | ||||
| import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; | ||||
| import 'package:rhythm_box/services/utils.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
|  | ||||
| @@ -55,6 +56,12 @@ abstract class SourcedTrack extends Track { | ||||
|         .cast<SourceInfo>(); | ||||
|  | ||||
|     return switch (audioSource) { | ||||
|       AudioSource.netease => NeteaseSourcedTrack( | ||||
|           source: source, | ||||
|           siblings: siblings, | ||||
|           sourceInfo: sourceInfo, | ||||
|           track: track, | ||||
|         ), | ||||
|       AudioSource.piped => PipedSourcedTrack( | ||||
|           source: source, | ||||
|           siblings: siblings, | ||||
| @@ -94,14 +101,20 @@ abstract class SourcedTrack extends Track { | ||||
|  | ||||
|     try { | ||||
|       return switch (audioSource) { | ||||
|         AudioSource.netease => | ||||
|           await NeteaseSourcedTrack.fetchFromTrack(track: track), | ||||
|         AudioSource.piped => | ||||
|           await PipedSourcedTrack.fetchFromTrack(track: track), | ||||
|         _ => await YoutubeSourcedTrack.fetchFromTrack(track: track), | ||||
|       }; | ||||
|     } on TrackNotFoundError catch (_) { | ||||
|       // TODO Try to look it up in other source | ||||
|       // But the youtube and piped.video are the same, and there is no extra sources, so i ignored this for temporary | ||||
|       rethrow; | ||||
|       return switch (preferences.audioSource) { | ||||
|         AudioSource.piped || | ||||
|         AudioSource.youtube => | ||||
|           await NeteaseSourcedTrack.fetchFromTrack(track: track), | ||||
|         AudioSource.netease => | ||||
|           await YoutubeSourcedTrack.fetchFromTrack(track: track), | ||||
|       }; | ||||
|     } on HttpClientClosedException catch (_) { | ||||
|       return await PipedSourcedTrack.fetchFromTrack(track: track); | ||||
|     } on VideoUnplayableException catch (_) { | ||||
|   | ||||
							
								
								
									
										235
									
								
								lib/services/sourced_track/sources/netease.dart
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										235
									
								
								lib/services/sourced_track/sources/netease.dart
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,235 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:get/get.dart' hide Value; | ||||
| import 'package:rhythm_box/providers/database.dart'; | ||||
| import 'package:rhythm_box/providers/user_preferences.dart'; | ||||
| import 'package:rhythm_box/services/database/database.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
| import 'package:rhythm_box/services/sourced_track/enums.dart'; | ||||
| import 'package:rhythm_box/services/sourced_track/exceptions.dart'; | ||||
| import 'package:rhythm_box/services/sourced_track/models/source_info.dart'; | ||||
| import 'package:rhythm_box/services/sourced_track/models/source_map.dart'; | ||||
| import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; | ||||
|  | ||||
| class NeteaseSourceInfo extends SourceInfo { | ||||
|   NeteaseSourceInfo({ | ||||
|     required super.id, | ||||
|     required super.title, | ||||
|     required super.artist, | ||||
|     required super.thumbnail, | ||||
|     required super.pageUrl, | ||||
|     required super.duration, | ||||
|     required super.artistUrl, | ||||
|     required super.album, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| class NeteaseSourcedTrack extends SourcedTrack { | ||||
|   NeteaseSourcedTrack({ | ||||
|     required super.source, | ||||
|     required super.siblings, | ||||
|     required super.sourceInfo, | ||||
|     required super.track, | ||||
|   }); | ||||
|  | ||||
|   static String _getBaseUrl() { | ||||
|     final preferences = Get.find<UserPreferencesProvider>().state.value; | ||||
|     return preferences.neteaseApiInstance; | ||||
|   } | ||||
|  | ||||
|   static GetConnect _getClient() { | ||||
|     final client = GetConnect(); | ||||
|     client.baseUrl = _getBaseUrl(); | ||||
|     client.timeout = const Duration(seconds: 30); | ||||
|     return client; | ||||
|   } | ||||
|  | ||||
|   static String? _lookedUpRealIp; | ||||
|  | ||||
|   static Future<String> lookupRealIp() async { | ||||
|     if (_lookedUpRealIp != null) return _lookedUpRealIp!; | ||||
|     const ipCheckUrl = 'https://api.ipify.org'; | ||||
|     final client = GetConnect(timeout: const Duration(seconds: 30)); | ||||
|     final resp = await client.get(ipCheckUrl); | ||||
|     _lookedUpRealIp = resp.body; | ||||
|     return _lookedUpRealIp!; | ||||
|   } | ||||
|  | ||||
|   static Future<NeteaseSourcedTrack> fetchFromTrack({ | ||||
|     required Track track, | ||||
|   }) async { | ||||
|     final DatabaseProvider db = Get.find(); | ||||
|     final cachedSource = await (db.database.select(db.database.sourceMatchTable) | ||||
|           ..where((s) => s.trackId.equals(track.id!)) | ||||
|           ..limit(1) | ||||
|           ..orderBy([ | ||||
|             (s) => | ||||
|                 OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), | ||||
|           ])) | ||||
|         .get() | ||||
|         .then((s) => s.firstOrNull); | ||||
|  | ||||
|     if (cachedSource == null) { | ||||
|       final siblings = await fetchSiblings(track: track); | ||||
|       if (siblings.isEmpty) { | ||||
|         throw TrackNotFoundError(track); | ||||
|       } | ||||
|  | ||||
|       await db.database.into(db.database.sourceMatchTable).insert( | ||||
|             SourceMatchTableCompanion.insert( | ||||
|               trackId: track.id!, | ||||
|               sourceId: siblings.first.info.id, | ||||
|               sourceType: const Value(SourceType.netease), | ||||
|             ), | ||||
|           ); | ||||
|  | ||||
|       return NeteaseSourcedTrack( | ||||
|         siblings: siblings.map((s) => s.info).skip(1).toList(), | ||||
|         source: siblings.first.source as SourceMap, | ||||
|         sourceInfo: siblings.first.info, | ||||
|         track: track, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final client = _getClient(); | ||||
|     final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}'); | ||||
|     final item = resp.body['songs'][0]; | ||||
|  | ||||
|     return NeteaseSourcedTrack( | ||||
|       siblings: [], | ||||
|       source: toSourceMap(item), | ||||
|       sourceInfo: NeteaseSourceInfo( | ||||
|         id: item['id'].toString(), | ||||
|         artist: item['ar'].map((x) => x['name']).join(','), | ||||
|         artistUrl: 'https://music.163.com/#/artist?id=${item['ar'][0]['id']}', | ||||
|         pageUrl: 'https://music.163.com/#/song?id=${item['id']}', | ||||
|         thumbnail: item['al']['picUrl'], | ||||
|         title: item['name'], | ||||
|         duration: Duration(milliseconds: item['dt']), | ||||
|         album: item['al']['name'], | ||||
|       ), | ||||
|       track: track, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static SourceMap toSourceMap(dynamic manifest) { | ||||
|     final baseUrl = _getBaseUrl(); | ||||
|  | ||||
|     // Due to netease may provide m4a, mp3 and others, we cannot decide this so mock this data. | ||||
|     return SourceMap( | ||||
|       m4a: SourceQualityMap( | ||||
|         high: '$baseUrl/song/url?id=${manifest['id']}', | ||||
|         medium: '$baseUrl/song/url?id=${manifest['id']}&br=192000', | ||||
|         low: '$baseUrl/song/url?id=${manifest['id']}&br=128000', | ||||
|       ), | ||||
|       weba: SourceQualityMap( | ||||
|         high: '$baseUrl/song/url?id=${manifest['id']}', | ||||
|         medium: '$baseUrl/song/url?id=${manifest['id']}&br=192000', | ||||
|         low: '$baseUrl/song/url?id=${manifest['id']}&br=128000', | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static Future<List<SiblingType>> fetchSiblings({ | ||||
|     required Track track, | ||||
|   }) async { | ||||
|     final query = SourcedTrack.getSearchTerm(track); | ||||
|  | ||||
|     final client = _getClient(); | ||||
|     final resp = | ||||
|         await client.get('/search?keywords=${Uri.encodeComponent(query)}'); | ||||
|     final results = resp.body['result']['songs']; | ||||
|  | ||||
|     // We can just trust netease music for now | ||||
|     // If we need to check is the result correct, refer to this code | ||||
|     // https://github.com/KRTirtho/spotube/blob/9b024120601c0d381edeab4460cb22f87149d0f8/lib/services/sourced_track/sources/jiosaavn.dart#L129 | ||||
|     final matchedResults = results.map(toSiblingType).toList(); | ||||
|  | ||||
|     return matchedResults.cast<SiblingType>(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<NeteaseSourcedTrack> copyWithSibling() async { | ||||
|     if (siblings.isNotEmpty) { | ||||
|       return this; | ||||
|     } | ||||
|     final fetchedSiblings = await fetchSiblings(track: this); | ||||
|  | ||||
|     return NeteaseSourcedTrack( | ||||
|       siblings: fetchedSiblings | ||||
|           .where((s) => s.info.id != sourceInfo.id) | ||||
|           .map((s) => s.info) | ||||
|           .toList(), | ||||
|       source: source, | ||||
|       sourceInfo: sourceInfo, | ||||
|       track: this, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<NeteaseSourcedTrack?> swapWithSibling(SourceInfo sibling) async { | ||||
|     if (sibling.id == sourceInfo.id) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     // a sibling source that was fetched from the search results | ||||
|     final isStepSibling = siblings.none((s) => s.id == sibling.id); | ||||
|  | ||||
|     final newSourceInfo = isStepSibling | ||||
|         ? sibling | ||||
|         : siblings.firstWhere((s) => s.id == sibling.id); | ||||
|     final newSiblings = siblings.where((s) => s.id != sibling.id).toList() | ||||
|       ..insert(0, sourceInfo); | ||||
|  | ||||
|     final client = _getClient(); | ||||
|     final resp = await client.get('/song/detail?ids=${newSourceInfo.id}'); | ||||
|     final item = resp.body['songs'][0]; | ||||
|  | ||||
|     final (:info, :source) = toSiblingType(item); | ||||
|  | ||||
|     final db = Get.find<DatabaseProvider>(); | ||||
|     await db.database.into(db.database.sourceMatchTable).insert( | ||||
|           SourceMatchTableCompanion.insert( | ||||
|             trackId: id!, | ||||
|             sourceId: info.id, | ||||
|             sourceType: const Value(SourceType.netease), | ||||
|             // Because we're sorting by createdAt in the query | ||||
|             // we have to update it to indicate priority | ||||
|             createdAt: Value(DateTime.now()), | ||||
|           ), | ||||
|           mode: InsertMode.replace, | ||||
|         ); | ||||
|  | ||||
|     return NeteaseSourcedTrack( | ||||
|       siblings: newSiblings, | ||||
|       source: source!, | ||||
|       sourceInfo: info, | ||||
|       track: this, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static SiblingType toSiblingType(dynamic item) { | ||||
|     final firstArtist = item['ar'] != null ? item['ar'][0] : item['artists'][0]; | ||||
|  | ||||
|     final SiblingType sibling = ( | ||||
|       info: NeteaseSourceInfo( | ||||
|         id: item['id'].toString(), | ||||
|         artist: item['ar'] != null | ||||
|             ? item['ar'].map((x) => x['name']).join(',') | ||||
|             : item['artists'].map((x) => x['name']).toString(), | ||||
|         artistUrl: 'https://music.163.com/#/artist?id=${firstArtist['id']}', | ||||
|         pageUrl: 'https://music.163.com/#/song?id=${item['id']}', | ||||
|         thumbnail: item['al']?['picUrl'] ?? | ||||
|             'https://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg', | ||||
|         title: item['name'], | ||||
|         duration: item['dt'] != null | ||||
|             ? Duration(milliseconds: item['dt']) | ||||
|             : Duration.zero, | ||||
|         album: item['al']?['name'], | ||||
|       ), | ||||
|       source: toSourceMap(item), | ||||
|     ); | ||||
|  | ||||
|     return sibling; | ||||
|   } | ||||
| } | ||||
| @@ -85,7 +85,7 @@ class YoutubeSourcedTrack extends SourcedTrack { | ||||
|           cachedSource.sourceId, | ||||
|         ) | ||||
|         .timeout( | ||||
|           const Duration(seconds: 5), | ||||
|           const Duration(seconds: 30), | ||||
|           onTimeout: () => throw ClientException('Timeout'), | ||||
|         ); | ||||
|     return YoutubeSourcedTrack( | ||||
| @@ -140,7 +140,7 @@ class YoutubeSourcedTrack extends SourcedTrack { | ||||
|     if (index == 0) { | ||||
|       final manifest = | ||||
|           await youtubeClient.videos.streamsClient.getManifest(item.id).timeout( | ||||
|                 const Duration(seconds: 5), | ||||
|                 const Duration(seconds: 30), | ||||
|                 onTimeout: () => throw ClientException('Timeout'), | ||||
|               ); | ||||
|       sourceMap = toSourceMap(manifest); | ||||
| @@ -285,7 +285,7 @@ class YoutubeSourcedTrack extends SourcedTrack { | ||||
|     final manifest = await youtubeClient.videos.streamsClient | ||||
|         .getManifest(newSourceInfo.id) | ||||
|         .timeout( | ||||
|           const Duration(seconds: 5), | ||||
|           const Duration(seconds: 30), | ||||
|           onTimeout: () => throw ClientException('Timeout'), | ||||
|         ); | ||||
|  | ||||
|   | ||||
							
								
								
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -347,10 +347,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dio | ||||
|       sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" | ||||
|       sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.6.0" | ||||
|     version: "5.7.0" | ||||
|   dio_web_adapter: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -383,6 +383,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.19.1" | ||||
|   dropdown_button2: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dropdown_button2 | ||||
|       sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.9" | ||||
|   duration: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1514,10 +1522,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: uuid | ||||
|       sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" | ||||
|       sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.4.2" | ||||
|     version: "4.5.0" | ||||
|   vector_math: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -101,6 +101,7 @@ dependencies: | ||||
|   timezone: ^0.9.4 | ||||
|   url_launcher: ^6.3.0 | ||||
|   wakelock_plus: ^1.2.8 | ||||
|   dropdown_button2: ^2.3.9 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user