3 Commits

Author SHA1 Message Date
222d50d80d 🐛 Bug fixes of querying backend 2024-09-06 18:10:12 +08:00
499bca5b1c Better netease music check 2024-09-06 16:37:49 +08:00
252e4619f7 🐛 Fix player view layout issue 2024-09-06 16:22:39 +08:00
9 changed files with 260 additions and 218 deletions

View File

@@ -1,59 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>ITSAppUsesNonExemptEncryption</key>
<true/> <false/>
<key>CFBundleDevelopmentRegion</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <true/>
<key>CFBundleDisplayName</key> <key>CFBundleDevelopmentRegion</key>
<string>Groovy Box</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleDisplayName</key>
<string>$(EXECUTABLE_NAME)</string> <string>Groovy Box</string>
<key>CFBundleIdentifier</key> <key>CFBundleExecutable</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleIdentifier</key>
<string>6.0</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key> <key>CFBundleInfoDictionaryVersion</key>
<string>Groovy Box</string> <string>6.0</string>
<key>CFBundlePackageType</key> <key>CFBundleName</key>
<string>APPL</string> <string>Groovy Box</string>
<key>CFBundleShortVersionString</key> <key>CFBundlePackageType</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>APPL</string>
<key>CFBundleSignature</key> <key>CFBundleShortVersionString</key>
<string>????</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key> <key>CFBundleSignature</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>????</string>
<key>LSApplicationCategoryType</key> <key>CFBundleVersion</key>
<string>public.app-category.music</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSApplicationCategoryType</key>
<true/> <string>public.app-category.music</string>
<key>NSMicrophoneUsageDescription</key> <key>LSRequiresIPhoneOS</key>
<string>To provide information for RhythmBox to normalize the output audio</string> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>NSMicrophoneUsageDescription</key>
<true/> <string>To provide information for RhythmBox to normalize the output audio</string>
<key>UIBackgroundModes</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<array> <true/>
<string>audio</string> <key>UIBackgroundModes</key>
</array> <array>
<key>UILaunchStoryboardName</key> <string>audio</string>
<string>LaunchScreen</string> </array>
<key>UIMainStoryboardFile</key> <key>UILaunchStoryboardName</key>
<string>Main</string> <string>LaunchScreen</string>
<key>UIStatusBarHidden</key> <key>UIMainStoryboardFile</key>
<false/> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UIStatusBarHidden</key>
<array> <false/>
<string>UIInterfaceOrientationPortrait</string> <key>UISupportedInterfaceOrientations</key>
<string>UIInterfaceOrientationLandscapeLeft</string> <array>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationPortrait</string>
</array> <string>UIInterfaceOrientationLandscapeLeft</string>
<key>UISupportedInterfaceOrientations~ipad</key> <string>UIInterfaceOrientationLandscapeRight</string>
<array> </array>
<string>UIInterfaceOrientationPortrait</string> <key>UISupportedInterfaceOrientations~ipad</key>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <array>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
</array> <string>UIInterfaceOrientationLandscapeLeft</string>
</dict> <string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist> </plist>

View File

@@ -82,34 +82,38 @@ class _PlayerScreenState extends State<PlayerScreen> {
padding: const EdgeInsets.symmetric(vertical: 24), padding: const EdgeInsets.symmetric(vertical: 24),
children: [ children: [
Obx( Obx(
() => LimitedBox( () => Center(
maxHeight: maxAlbumSize, child: LimitedBox(
maxWidth: maxAlbumSize, maxHeight: maxAlbumSize,
child: Hero( maxWidth: maxAlbumSize,
tag: const Key('current-active-track-album-art'), child: Hero(
child: AspectRatio( tag: const Key('current-active-track-album-art'),
aspectRatio: 1, child: AspectRatio(
child: ClipRRect( aspectRatio: 1,
borderRadius: child: ClipRRect(
const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(
child: _albumArt != null Radius.circular(16),
? AutoCacheImage( ),
_albumArt!, child: _albumArt != null
width: albumSize, ? AutoCacheImage(
height: albumSize, _albumArt!,
) width: albumSize,
: Container( height: albumSize,
color: Theme.of(context) fit: BoxFit.cover,
.colorScheme )
.surfaceContainerHigh, : Container(
width: 64, color: Theme.of(context)
height: 64, .colorScheme
child: const Center( .surfaceContainerHigh,
child: Icon(Icons.image), width: 64,
height: 64,
child: const Center(
child: Icon(Icons.image),
),
), ),
), ),
), ).marginSymmetric(horizontal: 24),
).marginSymmetric(horizontal: 24), ),
), ),
), ),
), ),
@@ -309,87 +313,89 @@ class _PlayerScreenState extends State<PlayerScreen> {
], ],
), ),
const Gap(20), const Gap(20),
SizedBox( Center(
height: 40, child: SizedBox(
child: ListView( height: 40,
scrollDirection: Axis.horizontal, child: ListView(
shrinkWrap: true, scrollDirection: Axis.horizontal,
children: [ shrinkWrap: true,
TextButton.icon( children: [
icon: const Icon(Icons.queue_music),
label: const Text(
'Queue',
maxLines: 1,
overflow: TextOverflow.fade,
),
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) =>
const PlayerQueuePopup(),
).then((_) {
if (mounted) {
setState(() {});
}
});
},
),
if (!isLargeScreen) const Gap(4),
if (!isLargeScreen)
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.lyrics), icon: const Icon(Icons.queue_music),
label: const Text( label: const Text(
'Lyrics', 'Queue',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
), ),
onPressed: () { onPressed: () {
GoRouter.of(context) showModalBottomSheet(
.pushNamed('playerLyrics'); useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) =>
const PlayerQueuePopup(),
).then((_) {
if (mounted) {
setState(() {});
}
});
}, },
), ),
const Gap(4), if (!isLargeScreen) const Gap(4),
TextButton.icon( if (!isLargeScreen)
icon: const Icon(Icons.merge), TextButton.icon(
label: const Text( icon: const Icon(Icons.lyrics),
'Sources', label: const Text(
maxLines: 1, 'Lyrics',
overflow: TextOverflow.fade, maxLines: 1,
overflow: TextOverflow.fade,
),
onPressed: () {
GoRouter.of(context)
.pushNamed('playerLyrics');
},
),
const Gap(4),
TextButton.icon(
icon: const Icon(Icons.merge),
label: const Text(
'Sources',
maxLines: 1,
overflow: TextOverflow.fade,
),
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) =>
const SiblingTracksPopup(),
).then((_) {
if (mounted) {
setState(() {});
}
});
},
), ),
onPressed: () { const Gap(4),
showModalBottomSheet( TextButton.icon(
useRootNavigator: true, label: const Text('Info'),
isScrollControlled: true, icon: const Icon(Icons.info),
context: context, onPressed: () {
builder: (context) => showModalBottomSheet(
const SiblingTracksPopup(), useRootNavigator: true,
).then((_) { context: context,
if (mounted) { builder: (context) =>
setState(() {}); const SourceDetailsPopup(),
} ).then((_) {
}); if (mounted) {
}, setState(() {});
), }
const Gap(4), });
TextButton.icon( },
label: const Text('Info'), ),
icon: const Icon(Icons.info), ],
onPressed: () { ),
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) =>
const SourceDetailsPopup(),
).then((_) {
if (mounted) {
setState(() {});
}
});
},
),
],
), ),
), ),
], ],

View File

@@ -109,8 +109,9 @@ abstract class SourcedTrack extends Track {
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track), _ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
}; };
} on TrackNotFoundError catch (err) { } on TrackNotFoundError catch (err) {
Get.find<ErrorNotifier>() Get.find<ErrorNotifier>().showError(
.showError('${err.toString()} via ${preferences.audioSource.label}'); '${err.toString()} via ${preferences.audioSource.label}, querying in fallback sources...',
);
return switch (preferences.audioSource) { return switch (preferences.audioSource) {
AudioSource.piped || AudioSource.piped ||
AudioSource.youtube => AudioSource.youtube =>

View File

@@ -93,6 +93,12 @@ class NeteaseSourcedTrack extends SourcedTrack {
throw TrackNotFoundError(track); throw TrackNotFoundError(track);
} }
final client = getClient();
final checkResp = await client.get(
'/check/music?id=${siblings.first.info.id}&realIP=${await lookupRealIp()}',
);
if (checkResp.body['success'] != true) throw TrackNotFoundError(track);
await db.database.into(db.database.sourceMatchTable).insert( await db.database.into(db.database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert( SourceMatchTableCompanion.insert(
trackId: track.id!, trackId: track.id!,
@@ -111,8 +117,14 @@ class NeteaseSourcedTrack extends SourcedTrack {
final client = getClient(); final client = getClient();
final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}'); final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}');
print(resp.body); final item = (resp.body['songs'] as List<dynamic>).firstOrNull;
final item = resp.body['songs'][0];
if (item == null) throw TrackNotFoundError(track);
final checkResp = await client.get(
'/check/music?id=${item['id']}&realIP=${await lookupRealIp()}',
);
if (checkResp.body['success'] != true) throw TrackNotFoundError(track);
return NeteaseSourcedTrack( return NeteaseSourcedTrack(
siblings: [], siblings: [],
@@ -155,8 +167,11 @@ class NeteaseSourcedTrack extends SourcedTrack {
final query = SourcedTrack.getSearchTerm(track); final query = SourcedTrack.getSearchTerm(track);
final client = getClient(); final client = getClient();
final resp = final resp = await client.get(
await client.get('/search?keywords=${Uri.encodeComponent(query)}'); '/search?keywords=${Uri.encodeComponent(query)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}',
);
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
print(resp.body);
final results = resp.body['result']['songs']; final results = resp.body['result']['songs'];
// We can just trust netease music for now // We can just trust netease music for now
@@ -202,7 +217,7 @@ class NeteaseSourcedTrack extends SourcedTrack {
final client = getClient(); final client = getClient();
final resp = await client.get('/song/detail?ids=${newSourceInfo.id}'); final resp = await client.get('/song/detail?ids=${newSourceInfo.id}');
final item = resp.body['songs'][0]; final item = (resp.body['songs'] as List<dynamic>).first;
final (:info, :source) = toSiblingType(item); final (:info, :source) = toSiblingType(item);
@@ -227,25 +242,29 @@ class NeteaseSourcedTrack extends SourcedTrack {
); );
} }
static SiblingType toSiblingType(dynamic item) { static NeteaseSourceInfo toSourceInfo(dynamic item) {
final firstArtist = item['ar'] != null ? item['ar'][0] : item['artists'][0]; final firstArtist = item['ar'] != null ? item['ar'][0] : item['artists'][0];
return 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(milliseconds: item['duration']),
album: item['al']?['name'],
);
}
static SiblingType toSiblingType(dynamic item) {
final SiblingType sibling = ( final SiblingType sibling = (
info: NeteaseSourceInfo( info: toSourceInfo(item),
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(milliseconds: item['duration']),
album: item['al']?['name'],
),
source: toSourceMap(item), source: toSourceMap(item),
); );

View File

@@ -69,6 +69,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
sourceId: siblings.first.info.id, sourceId: siblings.first.info.id,
sourceType: const Value(SourceType.youtube), sourceType: const Value(SourceType.youtube),
), ),
mode: InsertMode.insertOrReplace,
); );
return YoutubeSourcedTrack( return YoutubeSourcedTrack(

View File

@@ -5,8 +5,10 @@ import 'package:rhythm_box/platform.dart';
class AutoCacheImage extends StatelessWidget { class AutoCacheImage extends StatelessWidget {
final String url; final String url;
final double? width, height; final double? width, height;
final BoxFit? fit;
const AutoCacheImage(this.url, {super.key, this.width, this.height}); const AutoCacheImage(this.url,
{super.key, this.width, this.height, this.fit});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -15,12 +17,14 @@ class AutoCacheImage extends StatelessWidget {
imageUrl: url, imageUrl: url,
width: width, width: width,
height: height, height: height,
fit: fit,
); );
} }
return Image.network( return Image.network(
url, url,
width: width, width: width,
height: height, height: height,
fit: fit,
); );
} }

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/audio_player.dart';
import 'package:rhythm_box/providers/error_notifier.dart';
import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/providers/user_preferences.dart';
import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/database/database.dart';
import 'package:rhythm_box/services/duration.dart'; import 'package:rhythm_box/services/duration.dart';
@@ -89,48 +90,54 @@ class _SiblingTracksState extends State<SiblingTracks> {
final preferences = Get.find<UserPreferencesProvider>().state.value; final preferences = Get.find<UserPreferencesProvider>().state.value;
final searchTerm = _searchTermController.text.trim(); final searchTerm = _searchTermController.text.trim();
if (preferences.audioSource == AudioSource.youtube || try {
preferences.audioSource == AudioSource.piped) { if (preferences.audioSource == AudioSource.youtube ||
final resultsYt = await youtubeClient.search.search(searchTerm.trim()); preferences.audioSource == AudioSource.piped) {
final resultsYt = await youtubeClient.search.search(searchTerm.trim());
final searchResults = await Future.wait( final searchResults = await Future.wait(
resultsYt.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async { resultsYt
final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video); .map(YoutubeVideoInfo.fromVideo)
return siblingType.info; .mapIndexed((i, video) async {
}), final siblingType =
); await YoutubeSourcedTrack.toSiblingType(i, video);
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo; return siblingType.info;
_siblings = List.from( }),
searchResults );
..removeWhere((element) => element.id == activeSourceInfo.id) final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
..insert( _siblings = List.from(
0, searchResults
activeSourceInfo, ..removeWhere((element) => element.id == activeSourceInfo.id)
), ..insert(
growable: true, 0,
); activeSourceInfo,
} else if (preferences.audioSource == AudioSource.netease) { ),
final client = NeteaseSourcedTrack.getClient(); growable: true,
final resp = await client );
.get('/search?keywords=${Uri.encodeComponent(searchTerm)}'); } else if (preferences.audioSource == AudioSource.netease) {
final searchResults = resp.body['result']['songs'] final client = NeteaseSourcedTrack.getClient();
.map(NeteaseSourcedTrack.toSiblingType) final resp = await client.get(
.map((x) => x.info) '/search?keywords=${Uri.encodeComponent(searchTerm)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}');
.toList(); final searchResults = resp.body['result']['songs']
.map(NeteaseSourcedTrack.toSourceInfo)
.toList();
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo; final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
_siblings = List.from( _siblings = List.from(
searchResults searchResults
..removeWhere((element) => element.id == activeSourceInfo.id) ..removeWhere((element) => element.id == activeSourceInfo.id)
..insert( ..insert(
0, 0,
activeSourceInfo, activeSourceInfo,
), ),
growable: true, growable: true,
); );
}
} catch (err) {
Get.find<ErrorNotifier>().showError(err.toString());
} finally {
setState(() => _isSearching = false);
} }
setState(() => _isSearching = false);
} }
@override @override

View File

@@ -28,6 +28,8 @@
<string>MainMenu</string> <string>MainMenu</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.music</string> <string>public.app-category.music</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
</dict> </dict>

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+10 version: 1.0.0+11
environment: environment:
sdk: ^3.5.0 sdk: ^3.5.0