mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-12 02:37:18 +02:00
Initial commit: SpotiFLAC Android/iOS app
This commit is contained in:
commit
3fdcf6d5b7
126 files changed
+14079
No files matched your search
@@ -0,0 +1,122 @@
|
||||
import 'dart:io';
|
||||
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
|
||||
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
|
||||
|
||||
/// FFmpeg service for audio conversion and remuxing
|
||||
class FFmpegService {
|
||||
/// Convert M4A (DASH segments) to FLAC
|
||||
/// Returns the output file path on success, null on failure
|
||||
static Future<String?> convertM4aToFlac(String inputPath) async {
|
||||
final outputPath = inputPath.replaceAll('.m4a', '.flac');
|
||||
|
||||
// FFmpeg command to remux M4A to FLAC
|
||||
final command =
|
||||
'-i "$inputPath" -c:a flac -compression_level 8 "$outputPath" -y';
|
||||
|
||||
final session = await FFmpegKit.execute(command);
|
||||
final returnCode = await session.getReturnCode();
|
||||
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
// Delete original M4A file
|
||||
try {
|
||||
await File(inputPath).delete();
|
||||
} catch (_) {}
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Log error for debugging
|
||||
final logs = await session.getLogs();
|
||||
for (final log in logs) {
|
||||
print('[FFmpeg] ${log.getMessage()}');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Convert FLAC to MP3
|
||||
static Future<String?> convertFlacToMp3(
|
||||
String inputPath, {
|
||||
String bitrate = '320k',
|
||||
}) async {
|
||||
final dir = File(inputPath).parent.path;
|
||||
final baseName =
|
||||
inputPath.split(Platform.pathSeparator).last.replaceAll('.flac', '');
|
||||
final outputDir = '$dir${Platform.pathSeparator}MP3';
|
||||
|
||||
// Create output directory
|
||||
await Directory(outputDir).create(recursive: true);
|
||||
|
||||
final outputPath = '$outputDir${Platform.pathSeparator}$baseName.mp3';
|
||||
|
||||
final command =
|
||||
'-i "$inputPath" -codec:a libmp3lame -b:a $bitrate -map 0:a -map_metadata 0 -id3v2_version 3 "$outputPath" -y';
|
||||
|
||||
final session = await FFmpegKit.execute(command);
|
||||
final returnCode = await session.getReturnCode();
|
||||
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Convert FLAC to M4A (AAC or ALAC)
|
||||
static Future<String?> convertFlacToM4a(
|
||||
String inputPath, {
|
||||
String codec = 'aac',
|
||||
String bitrate = '256k',
|
||||
}) async {
|
||||
final dir = File(inputPath).parent.path;
|
||||
final baseName =
|
||||
inputPath.split(Platform.pathSeparator).last.replaceAll('.flac', '');
|
||||
final outputDir = '$dir${Platform.pathSeparator}M4A';
|
||||
|
||||
// Create output directory
|
||||
await Directory(outputDir).create(recursive: true);
|
||||
|
||||
final outputPath = '$outputDir${Platform.pathSeparator}$baseName.m4a';
|
||||
|
||||
String command;
|
||||
if (codec == 'alac') {
|
||||
// ALAC - lossless
|
||||
command =
|
||||
'-i "$inputPath" -codec:a alac -map 0:a -map_metadata 0 "$outputPath" -y';
|
||||
} else {
|
||||
// AAC - lossy
|
||||
command =
|
||||
'-i "$inputPath" -codec:a aac -b:a $bitrate -map 0:a -map_metadata 0 "$outputPath" -y';
|
||||
}
|
||||
|
||||
final session = await FFmpegKit.execute(command);
|
||||
final returnCode = await session.getReturnCode();
|
||||
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if FFmpeg is available
|
||||
static Future<bool> isAvailable() async {
|
||||
try {
|
||||
final session = await FFmpegKit.execute('-version');
|
||||
final returnCode = await session.getReturnCode();
|
||||
return ReturnCode.isSuccess(returnCode);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get FFmpeg version info
|
||||
static Future<String?> getVersion() async {
|
||||
try {
|
||||
final session = await FFmpegKit.execute('-version');
|
||||
final output = await session.getOutput();
|
||||
return output;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Bridge to communicate with Go backend via platform channels
|
||||
class PlatformBridge {
|
||||
static const _channel = MethodChannel('com.zarz.spotiflac/backend');
|
||||
|
||||
/// Parse and validate Spotify URL
|
||||
static Future<Map<String, dynamic>> parseSpotifyUrl(String url) async {
|
||||
final result = await _channel.invokeMethod('parseSpotifyUrl', {'url': url});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Get Spotify metadata from URL
|
||||
static Future<Map<String, dynamic>> getSpotifyMetadata(String url) async {
|
||||
final result = await _channel.invokeMethod('getSpotifyMetadata', {'url': url});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Search Spotify
|
||||
static Future<Map<String, dynamic>> searchSpotify(String query, {int limit = 10}) async {
|
||||
final result = await _channel.invokeMethod('searchSpotify', {
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Check track availability on streaming services
|
||||
static Future<Map<String, dynamic>> checkAvailability(String spotifyId, String isrc) async {
|
||||
final result = await _channel.invokeMethod('checkAvailability', {
|
||||
'spotify_id': spotifyId,
|
||||
'isrc': isrc,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Download a track from specific service
|
||||
static Future<Map<String, dynamic>> downloadTrack({
|
||||
required String isrc,
|
||||
required String service,
|
||||
required String spotifyId,
|
||||
required String trackName,
|
||||
required String artistName,
|
||||
required String albumName,
|
||||
String? albumArtist,
|
||||
String? coverUrl,
|
||||
required String outputDir,
|
||||
required String filenameFormat,
|
||||
bool embedLyrics = true,
|
||||
bool embedMaxQualityCover = true,
|
||||
int trackNumber = 1,
|
||||
int discNumber = 1,
|
||||
int totalTracks = 1,
|
||||
String? releaseDate,
|
||||
}) async {
|
||||
final request = jsonEncode({
|
||||
'isrc': isrc,
|
||||
'service': service,
|
||||
'spotify_id': spotifyId,
|
||||
'track_name': trackName,
|
||||
'artist_name': artistName,
|
||||
'album_name': albumName,
|
||||
'album_artist': albumArtist ?? artistName,
|
||||
'cover_url': coverUrl,
|
||||
'output_dir': outputDir,
|
||||
'filename_format': filenameFormat,
|
||||
'embed_lyrics': embedLyrics,
|
||||
'embed_max_quality_cover': embedMaxQualityCover,
|
||||
'track_number': trackNumber,
|
||||
'disc_number': discNumber,
|
||||
'total_tracks': totalTracks,
|
||||
'release_date': releaseDate ?? '',
|
||||
});
|
||||
|
||||
final result = await _channel.invokeMethod('downloadTrack', request);
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Download with automatic fallback to other services
|
||||
static Future<Map<String, dynamic>> downloadWithFallback({
|
||||
required String isrc,
|
||||
required String spotifyId,
|
||||
required String trackName,
|
||||
required String artistName,
|
||||
required String albumName,
|
||||
String? albumArtist,
|
||||
String? coverUrl,
|
||||
required String outputDir,
|
||||
required String filenameFormat,
|
||||
bool embedLyrics = true,
|
||||
bool embedMaxQualityCover = true,
|
||||
int trackNumber = 1,
|
||||
int discNumber = 1,
|
||||
int totalTracks = 1,
|
||||
String? releaseDate,
|
||||
String preferredService = 'tidal',
|
||||
}) async {
|
||||
final request = jsonEncode({
|
||||
'isrc': isrc,
|
||||
'service': preferredService,
|
||||
'spotify_id': spotifyId,
|
||||
'track_name': trackName,
|
||||
'artist_name': artistName,
|
||||
'album_name': albumName,
|
||||
'album_artist': albumArtist ?? artistName,
|
||||
'cover_url': coverUrl,
|
||||
'output_dir': outputDir,
|
||||
'filename_format': filenameFormat,
|
||||
'embed_lyrics': embedLyrics,
|
||||
'embed_max_quality_cover': embedMaxQualityCover,
|
||||
'track_number': trackNumber,
|
||||
'disc_number': discNumber,
|
||||
'total_tracks': totalTracks,
|
||||
'release_date': releaseDate ?? '',
|
||||
});
|
||||
|
||||
final result = await _channel.invokeMethod('downloadWithFallback', request);
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Get download progress
|
||||
static Future<Map<String, dynamic>> getDownloadProgress() async {
|
||||
final result = await _channel.invokeMethod('getDownloadProgress');
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Set download directory
|
||||
static Future<void> setDownloadDirectory(String path) async {
|
||||
await _channel.invokeMethod('setDownloadDirectory', {'path': path});
|
||||
}
|
||||
|
||||
/// Check if file with ISRC already exists
|
||||
static Future<Map<String, dynamic>> checkDuplicate(String outputDir, String isrc) async {
|
||||
final result = await _channel.invokeMethod('checkDuplicate', {
|
||||
'output_dir': outputDir,
|
||||
'isrc': isrc,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Build filename from template
|
||||
static Future<String> buildFilename(String template, Map<String, dynamic> metadata) async {
|
||||
final result = await _channel.invokeMethod('buildFilename', {
|
||||
'template': template,
|
||||
'metadata': jsonEncode(metadata),
|
||||
});
|
||||
return result as String;
|
||||
}
|
||||
|
||||
/// Sanitize filename
|
||||
static Future<String> sanitizeFilename(String filename) async {
|
||||
final result = await _channel.invokeMethod('sanitizeFilename', {
|
||||
'filename': filename,
|
||||
});
|
||||
return result as String;
|
||||
}
|
||||
|
||||
/// Fetch lyrics for a track
|
||||
static Future<Map<String, dynamic>> fetchLyrics(
|
||||
String spotifyId,
|
||||
String trackName,
|
||||
String artistName,
|
||||
) async {
|
||||
final result = await _channel.invokeMethod('fetchLyrics', {
|
||||
'spotify_id': spotifyId,
|
||||
'track_name': trackName,
|
||||
'artist_name': artistName,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Get lyrics in LRC format
|
||||
static Future<String> getLyricsLRC(
|
||||
String spotifyId,
|
||||
String trackName,
|
||||
String artistName,
|
||||
) async {
|
||||
final result = await _channel.invokeMethod('getLyricsLRC', {
|
||||
'spotify_id': spotifyId,
|
||||
'track_name': trackName,
|
||||
'artist_name': artistName,
|
||||
});
|
||||
return result as String;
|
||||
}
|
||||
|
||||
/// Embed lyrics into an existing FLAC file
|
||||
static Future<Map<String, dynamic>> embedLyricsToFile(
|
||||
String filePath,
|
||||
String lyrics,
|
||||
) async {
|
||||
final result = await _channel.invokeMethod('embedLyricsToFile', {
|
||||
'file_path': filePath,
|
||||
'lyrics': lyrics,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user