├── .idea └── .idea.Alasa │ └── .idea │ ├── .gitignore │ ├── avalonia.xml │ ├── encodings.xml │ ├── indexLayout.xml │ └── vcs.xml ├── Alasa.sln ├── Alasa.sln.DotSettings ├── Alasa.sln.DotSettings.user ├── FishFM ├── .gitignore ├── AlasaHotKey.cs ├── App.axaml ├── App.axaml.cs ├── Assets │ ├── avalonia-logo.ico │ └── remixicon.ttf ├── Converter │ └── TrackTimeConverter.cs ├── FishFM.csproj ├── FishFM.icns ├── FishFM.sln ├── Helper │ └── DbHelper.cs ├── Models │ ├── AlbumInfo.cs │ ├── ArtistInfo.cs │ ├── ConfigInfo.cs │ ├── DbSong.cs │ ├── LikedSong.cs │ └── SongResult.cs ├── Program.cs ├── ViewLocator.cs ├── ViewModels │ ├── AppViewModel.cs │ ├── MainWindowViewModel.cs │ └── ViewModelBase.cs ├── Views │ ├── CaptureWIndow.axaml │ ├── CaptureWIndow.axaml.cs │ ├── MainWindow.axaml │ └── MainWindow.axaml.cs ├── bass.dll ├── libbass.dylib └── libbass.so └── README.md /.idea/.idea.Alasa/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /contentModel.xml 6 | /projectSettingsUpdater.xml 7 | /.idea.Alasa.iml 8 | /modules.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/.idea.Alasa/.idea/avalonia.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/.idea.Alasa/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.Alasa/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.Alasa/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Alasa.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishFM", "FishFM\FishFM.csproj", "{2BA01A00-9355-4F0D-BDE5-DD928368F40E}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {2BA01A00-9355-4F0D-BDE5-DD928368F40E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {2BA01A00-9355-4F0D-BDE5-DD928368F40E}.Release|Any CPU.ActiveCfg = Release|Any CPU 13 | {2BA01A00-9355-4F0D-BDE5-DD928368F40E}.Release|Any CPU.Build.0 = Release|Any CPU 14 | {2BA01A00-9355-4F0D-BDE5-DD928368F40E}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /Alasa.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | False 3 | True -------------------------------------------------------------------------------- /Alasa.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | <AssemblyExplorer> 5 | <Assembly Path="/Users/chops/Downloads/Bass24.Net/OSX/Bass.Net.OSX.dll" /> 6 | </AssemblyExplorer> 7 | /usr/local/share/dotnet/x64/dotnet 8 | 9 | 10 | /usr/local/share/dotnet/x64/sdk/3.1.419/MSBuild.dll -------------------------------------------------------------------------------- /FishFM/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .vs/ 4 | 5 | bin/ 6 | obj/ 7 | 8 | *.user -------------------------------------------------------------------------------- /FishFM/AlasaHotKey.cs: -------------------------------------------------------------------------------- 1 | using NHotkey; 2 | 3 | namespace Alasa; 4 | 5 | public class AlasaHotKey : HotkeyManagerBase 6 | { 7 | public AlasaHotKey() 8 | { 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /FishFM/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /FishFM/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.ApplicationLifetimes; 4 | using Avalonia.Markup.Xaml; 5 | using Avalonia.Media; 6 | using FishFM.ViewModels; 7 | using FishFM.Views; 8 | 9 | namespace FishFM 10 | { 11 | public class App : Application 12 | { 13 | public override void Initialize() 14 | { 15 | AvaloniaXamlLoader.Load(this); 16 | } 17 | 18 | public override void OnFrameworkInitializationCompleted() 19 | { 20 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 21 | { 22 | desktop.MainWindow = new MainWindow 23 | { 24 | DataContext = new MainWindowViewModel(), 25 | }; 26 | } 27 | 28 | base.OnFrameworkInitializationCompleted(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /FishFM/Assets/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/FishFM/6a222554976034b00421d82a188914353e032698/FishFM/Assets/avalonia-logo.ico -------------------------------------------------------------------------------- /FishFM/Assets/remixicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/FishFM/6a222554976034b00421d82a188914353e032698/FishFM/Assets/remixicon.ttf -------------------------------------------------------------------------------- /FishFM/Converter/TrackTimeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace FishFM.Converter 6 | { 7 | 8 | public class TrackTimeConverter : IValueConverter 9 | { 10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | return double.TryParse(value.ToString(), out var result) ? SecondsToTime((int) result) : "00:00:00"; 13 | } 14 | 15 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 16 | { 17 | return 0; 18 | } 19 | 20 | private static string SecondsToTime(int total) 21 | { 22 | var hour = total / 3600; 23 | var min = (total % 3600) / 60; 24 | var sec = (total % 3600) % 60; 25 | return hour.ToString().PadLeft(2, '0') + ":" + min.ToString().PadLeft(2, '0') + ":" + 26 | sec.ToString().PadLeft(2, '0'); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /FishFM/FishFM.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | netcoreapp3.1 5 | enable 6 | 0.0.1 7 | 0.0.1 8 | en-CN 9 | true 10 | Assets/avalonia-logo.ico 11 | 12 | FishFM 13 | FishFM 14 | fun.ifish 15 | 1.0.0 16 | APPL 17 | ???? 18 | FishFM 19 | FishFM.icns 20 | 1.0 21 | NSApplication 22 | true 23 | 65001 24 | 9 25 | 26 | 27 | 4 28 | true 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | CaptureWIndow.axaml 49 | Code 50 | 51 | 52 | 53 | 54 | 55 | ..\..\..\Downloads\Bass24.Net\standard\Bass.Net.dll 56 | 57 | 58 | 59 | 60 | PreserveNewest 61 | 62 | 63 | PreserveNewest 64 | 65 | 66 | PreserveNewest 67 | 68 | 69 | PreserveNewest 70 | 71 | 72 | 73 | 74 | 75 | README.md 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /FishFM/FishFM.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/FishFM/6a222554976034b00421d82a188914353e032698/FishFM/FishFM.icns -------------------------------------------------------------------------------- /FishFM/FishFM.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 25.0.1700.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishFM", "FishFM.csproj", "{6221C981-94F6-4BDA-A5A0-7AD555F787CF}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6221C981-94F6-4BDA-A5A0-7AD555F787CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {6221C981-94F6-4BDA-A5A0-7AD555F787CF}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {6221C981-94F6-4BDA-A5A0-7AD555F787CF}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {6221C981-94F6-4BDA-A5A0-7AD555F787CF}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {20BB610C-5608-4C84-94EF-097FE2C57DBA} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /FishFM/Helper/DbHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using FishFM.Models; 5 | using LiteDB; 6 | using Newtonsoft.Json; 7 | 8 | namespace FishFM.Helper 9 | { 10 | 11 | public class DbHelper 12 | { 13 | private const string FmSongTable = "t_fm_songs"; 14 | private const string LikeTable = "t_liked_songs"; 15 | private const string ConfigTable = "t_config_songs"; 16 | private static readonly string DbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "MyFM.db"); 17 | private static readonly Random Rdm = new Random(); 18 | 19 | public static bool UpsertSongs(List list, string date, string type) 20 | { 21 | var dbSongs = new List(list.Count); 22 | foreach (var songResult in list) 23 | { 24 | dbSongs.Add(songResult.ToDbSong(type, date)); 25 | } 26 | 27 | using var db = new LiteDatabase(DbPath); 28 | var col = db.GetCollection(FmSongTable); 29 | return col.Upsert(dbSongs) >= 0; 30 | } 31 | 32 | public static List GetSongs(string date, string type) 33 | { 34 | using var db = new LiteDatabase(DbPath); 35 | var col = db.GetCollection(FmSongTable); 36 | var list = col.Query() 37 | .Where(s => s.FmType == type && (string.IsNullOrEmpty(date) || s.AddDate == date)).ToList(); 38 | var results = new List(list.Count); 39 | foreach (var dbSong in list) 40 | { 41 | var song = JsonConvert.DeserializeObject(dbSong.Text); 42 | if (song != null) 43 | { 44 | results.Add(song); 45 | } 46 | } 47 | 48 | return results; 49 | } 50 | 51 | public static SongResult? GetLastSong(string type) 52 | { 53 | using var db = new LiteDatabase(DbPath); 54 | var col = db.GetCollection(FmSongTable); 55 | var lastSong = col.Query() 56 | .Where(s => s.FmType == type).OrderByDescending(s => s.AddDate).Limit(1).FirstOrDefault(); 57 | return lastSong == null ? null : JsonConvert.DeserializeObject(lastSong.Text); 58 | } 59 | 60 | public static List GetSongsByIds(List ids) 61 | { 62 | using var db = new LiteDatabase(DbPath); 63 | var col = db.GetCollection(FmSongTable); 64 | var list = col.Query() 65 | .Where(s => ids.Contains(s.Id)).ToList(); 66 | var results = new List(list.Count); 67 | foreach (var dbSong in list) 68 | { 69 | var song = JsonConvert.DeserializeObject(dbSong.Text); 70 | if (song != null) 71 | { 72 | results.Add(song); 73 | } 74 | } 75 | 76 | return results; 77 | } 78 | 79 | public static List GetRandomSongs() 80 | { 81 | using var db = new LiteDatabase(DbPath); 82 | var col = db.GetCollection(FmSongTable); 83 | var offset = Rdm.Next(0, col.Count()); 84 | var list = col.Query() 85 | .Limit(1).Offset(offset).ToList(); 86 | var results = new List(list.Count); 87 | foreach (var dbSong in list) 88 | { 89 | var song = JsonConvert.DeserializeObject(dbSong.Text); 90 | if (song != null) 91 | { 92 | results.Add(song); 93 | } 94 | } 95 | 96 | return results; 97 | } 98 | 99 | public static void LikeSong(SongResult songResult) 100 | { 101 | using var db = new LiteDatabase(DbPath); 102 | var col = db.GetCollection(LikeTable); 103 | col.Upsert(new LikedSong() {Id = songResult.ToDbSong("", "").Id}); 104 | } 105 | 106 | public static void DislikeSong(SongResult songResult) 107 | { 108 | using var db = new LiteDatabase(DbPath); 109 | var col = db.GetCollection(LikeTable); 110 | col.Delete(songResult.ToDbSong("", "").Id); 111 | } 112 | 113 | public static bool IsSongLiked(SongResult? songResult) 114 | { 115 | if (songResult == null || string.IsNullOrEmpty(songResult.Id)) 116 | { 117 | return false; 118 | } 119 | 120 | using var db = new LiteDatabase(DbPath); 121 | var col = db.GetCollection(LikeTable); 122 | return col.Exists(x => x.Id == songResult.ToDbSong("", "").Id); 123 | } 124 | 125 | public static List GetAllLikedSong() 126 | { 127 | using var db = new LiteDatabase(DbPath); 128 | var col = db.GetCollection(LikeTable); 129 | return col.Query().Select(x => x.Id).ToList(); 130 | } 131 | 132 | public static bool SetConfig(string id, string value) 133 | { 134 | using var db = new LiteDatabase(DbPath); 135 | var col = db.GetCollection(ConfigTable); 136 | return col.Upsert(new ConfigInfo() {Id = id, Value = value}); 137 | } 138 | 139 | public static string GetConfig(string id) 140 | { 141 | using var db = new LiteDatabase(DbPath); 142 | var col = db.GetCollection(ConfigTable); 143 | var list = col.Query().Where(c => c.Id == id).ToList(); 144 | return list.Count > 0 ? list[0].Value : ""; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /FishFM/Models/AlbumInfo.cs: -------------------------------------------------------------------------------- 1 | namespace FishFM.Models 2 | { 3 | public class AlbumInfo 4 | { 5 | public string Id { get; set; } 6 | public string Name { get; set; } 7 | public string SubName { get; set; } 8 | public string ArtistId { get; set; } 9 | public string ArtistName { get; set; } 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /FishFM/Models/ArtistInfo.cs: -------------------------------------------------------------------------------- 1 | namespace FishFM.Models 2 | { 3 | public class ArtistInfo 4 | { 5 | public string Id { get; set; } 6 | public string Name { get; set; } 7 | public string SubName { get; set; } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /FishFM/Models/ConfigInfo.cs: -------------------------------------------------------------------------------- 1 | namespace FishFM.Models 2 | { 3 | public class ConfigInfo 4 | { 5 | public string Id { get; set; } 6 | public string Value { get; set; } 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /FishFM/Models/DbSong.cs: -------------------------------------------------------------------------------- 1 | namespace FishFM.Models 2 | { 3 | public class DbSong 4 | { 5 | public string Id { get; set; } 6 | public string LocalPath { get; set; } 7 | public string AddDate { get; set; } 8 | public string FmType { get; set; } 9 | public string Text { get; set; } 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /FishFM/Models/LikedSong.cs: -------------------------------------------------------------------------------- 1 | namespace FishFM.Models 2 | { 3 | public class LikedSong 4 | { 5 | public string Id { get; set; } 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /FishFM/Models/SongResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace FishFM.Models 5 | { 6 | 7 | public class SongResult 8 | { 9 | /** 10 | * 歌曲ID 11 | **/ 12 | public string Id { get; set; } 13 | /** 14 | * 曲名 15 | **/ 16 | public string Name{ get; set; } 17 | 18 | public List ArtistInfo { get; set; } 19 | 20 | public AlbumInfo AlbumInfo { get; set; } 21 | 22 | /** 23 | * 歌曲别名 24 | **/ 25 | public string SubName{ get; set; } 26 | 27 | /** 28 | * 时长 29 | **/ 30 | public int Length{ get; set; } 31 | 32 | public string SongLength{ get; set; } 33 | 34 | /** 35 | * 比特率 36 | **/ 37 | public string BitRate{ get; set; } 38 | /** 39 | * Flac无损地址 40 | **/ 41 | public string FlacUrl{ get; set; } 42 | /** 43 | * Ape无损地址 44 | **/ 45 | public string ApeUrl{ get; set; } 46 | /** 47 | * Wav地址 48 | **/ 49 | public string WavUrl{ get; set; } 50 | /** 51 | * 320K 52 | **/ 53 | public string SqUrl{ get; set; } 54 | /** 55 | * 192K 56 | **/ 57 | public string HqUrl{ get; set; } 58 | /** 59 | * 128K 60 | **/ 61 | public string LqUrl{ get; set; } 62 | /** 63 | * 复制链接 64 | **/ 65 | public string CopyUrl{ get; set; } 66 | /** 67 | * 歌曲小封面120*120 68 | **/ 69 | public string SmallPic{ get; set; } 70 | /** 71 | * 歌曲封面 72 | **/ 73 | public string PicUrl{ get; set; } 74 | /** 75 | * LRC歌词 76 | **/ 77 | public string LrcUrl{ get; set; } 78 | /** 79 | * TRC歌词 80 | **/ 81 | public string TrcUrl{ get; set; } 82 | /** 83 | * KRC歌词 84 | **/ 85 | public string KrcUrl{ get; set; } 86 | /** 87 | * MV Id 88 | **/ 89 | public string MvId{ get; set; } 90 | /** 91 | * 高清MV地址 92 | **/ 93 | /// 94 | public string MvHdUrl{ get; set; } 95 | /** 96 | * 普清MV地址 97 | **/ 98 | public string MvLdUrl{ get; set; } 99 | /** 100 | * 语种 101 | **/ 102 | public string Language{ get; set; } 103 | /** 104 | * 发行公司 105 | **/ 106 | public string Company{ get; set; } 107 | /** 108 | * 歌曲发行日期 109 | **/ 110 | public string Year{ get; set; } 111 | /** 112 | * 碟片 113 | **/ 114 | public int Disc{ get; set; } 115 | /** 116 | * 曲目编号 117 | **/ 118 | public int TrackNum{ get; set; } 119 | /** 120 | * 类型 121 | **/ 122 | public string Type{ get; set; } 123 | 124 | public DbSong ToDbSong(string fmType, string date) 125 | { 126 | if (string.IsNullOrEmpty(Type)) 127 | { 128 | Type = PicUrl.Contains("/wy_") ? "wy" : "qq"; 129 | } 130 | return new DbSong 131 | { 132 | Id = Type + "#" + Id, 133 | FmType = fmType, 134 | AddDate = date, 135 | LocalPath = "", 136 | Text = JsonConvert.SerializeObject(this, Formatting.None) 137 | }; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /FishFM/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.ReactiveUI; 3 | 4 | namespace FishFM 5 | { 6 | class Program 7 | { 8 | // Initialization code. Don't use any Avalonia, third-party APIs or any 9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 10 | // yet and stuff might break. 11 | public static void Main(string[] args) => BuildAvaloniaApp() 12 | .StartWithClassicDesktopLifetime(args); 13 | 14 | // Avalonia configuration, don't remove; also used by visual designer. 15 | public static AppBuilder BuildAvaloniaApp() 16 | => AppBuilder.Configure() 17 | .UsePlatformDetect() 18 | .LogToTrace() 19 | .UseReactiveUI(); 20 | } 21 | } -------------------------------------------------------------------------------- /FishFM/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Templates; 4 | using FishFM.ViewModels; 5 | 6 | namespace FishFM 7 | { 8 | public class ViewLocator : IDataTemplate 9 | { 10 | public bool SupportsRecycling => false; 11 | 12 | public IControl Build(object data) 13 | { 14 | var name = data.GetType().FullName!.Replace("ViewModel", "View"); 15 | var type = Type.GetType(name); 16 | 17 | if (type != null) 18 | { 19 | return (Control) Activator.CreateInstance(type)!; 20 | } 21 | else 22 | { 23 | return new TextBlock {Text = "Not Found: " + name}; 24 | } 25 | } 26 | 27 | public bool Match(object data) 28 | { 29 | return data is ViewModelBase; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /FishFM/ViewModels/AppViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Themes.Fluent; 2 | using ReactiveUI; 3 | 4 | namespace FishFM.ViewModels 5 | { 6 | 7 | public class AppViewModel : ViewModelBase 8 | { 9 | private FluentThemeMode _themeMode = FluentThemeMode.Light; 10 | 11 | public FluentThemeMode ThemeMode 12 | { 13 | get => _themeMode; 14 | set => this.RaiseAndSetIfChanged(ref _themeMode, value); 15 | } 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /FishFM/ViewModels/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Avalonia; 8 | using Avalonia.Media.Imaging; 9 | using FishFM.Helper; 10 | using FishFM.Models; 11 | using Newtonsoft.Json; 12 | using ReactiveUI; 13 | using Un4seen.Bass; 14 | 15 | namespace FishFM.ViewModels 16 | { 17 | public class MainWindowViewModel : ViewModelBase 18 | { 19 | private readonly Random _random = new(); 20 | private DOWNLOADPROC? _downloadProc; 21 | private BASSTimer? _updateTimer; 22 | private int _playCount; 23 | private int _currentStream; 24 | private bool _isDiscoveryInit; 25 | 26 | #region Params 27 | 28 | private IBitmap? _albumPic; 29 | 30 | public IBitmap? AlbumPic 31 | { 32 | get => _albumPic; 33 | set => this.RaiseAndSetIfChanged(ref _albumPic, value); 34 | } 35 | 36 | private bool _playing; 37 | 38 | public bool Playing 39 | { 40 | get => _playing; 41 | set => this.RaiseAndSetIfChanged(ref _playing, value); 42 | } 43 | 44 | private List? _songList; 45 | 46 | private SongResult? _currentSong; 47 | 48 | public SongResult? CurrentSong 49 | { 50 | get => _currentSong; 51 | private set 52 | { 53 | this.RaiseAndSetIfChanged(ref _currentSong, value); 54 | UpdateLiked(); 55 | } 56 | } 57 | 58 | private void UpdateLiked() 59 | { 60 | Liked = DbHelper.IsSongLiked(CurrentSong); 61 | } 62 | 63 | private double _currentPosition; 64 | 65 | public double CurrentPosition 66 | { 67 | get => _currentPosition; 68 | set => this.RaiseAndSetIfChanged(ref _currentPosition, value); 69 | } 70 | 71 | private double _trackLength; 72 | 73 | public double TrackLength 74 | { 75 | get => _trackLength; 76 | set 77 | { 78 | if (Math.Abs(_trackLength - value) < 0.5) 79 | { 80 | return; 81 | } 82 | this.RaiseAndSetIfChanged(ref _trackLength, value); 83 | } 84 | } 85 | 86 | private double _processWidth; 87 | 88 | public double ProcessWidth 89 | { 90 | get => _processWidth; 91 | set => this.RaiseAndSetIfChanged(ref _processWidth, value); 92 | } 93 | 94 | private int _tabIndex; 95 | 96 | public int TabIndex 97 | { 98 | get => _tabIndex; 99 | set => this.RaiseAndSetIfChanged(ref _tabIndex, value); 100 | } 101 | 102 | private const string DefaultTip = "有的鱼是永远都关不住的,因为他们属于天空"; 103 | private string _tipText = DefaultTip; 104 | public string TipText 105 | { 106 | get => _tipText; 107 | set => this.RaiseAndSetIfChanged(ref _tipText, value); 108 | } 109 | 110 | private bool _liked; 111 | public bool Liked 112 | { 113 | get => _liked; 114 | set => this.RaiseAndSetIfChanged(ref _liked, value); 115 | } 116 | 117 | 118 | public MainWindowViewModel() 119 | { 120 | InitBassAndSongs(); 121 | } 122 | 123 | #endregion 124 | 125 | public void PlayPauseMusic() 126 | { 127 | if (Playing) 128 | { 129 | Pause(); 130 | } 131 | else 132 | { 133 | Play(); 134 | } 135 | } 136 | 137 | private void InitBassAndSongs() 138 | { 139 | Task.Factory.StartNew(() => 140 | { 141 | BassInit(); 142 | RefreshList(); 143 | }); 144 | } 145 | 146 | private void PlayRandom() 147 | { 148 | if (_songList == null) 149 | { 150 | return; 151 | } 152 | var index = _random.Next(0, _songList.Count); 153 | CurrentSong = _songList[index]; 154 | PlaySong(CurrentSong); 155 | } 156 | 157 | private void BassInit() 158 | { 159 | BassNet.Registration("shelher@163.com", "2X2831371512622"); 160 | if (!Bass.BASS_Init(-1, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero)) 161 | { 162 | return; 163 | } 164 | Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_TIMEOUT, 15000); 165 | Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_READTIMEOUT, 15000); 166 | _updateTimer = new BASSTimer(250); 167 | _updateTimer.Tick += UpdateTimerOnTick; 168 | _downloadProc = CachePlayingSong; 169 | } 170 | 171 | private void UpdateTimerOnTick(object? sender, EventArgs e) 172 | { 173 | if (_currentStream == 0) 174 | { 175 | return; 176 | } 177 | switch (Bass.BASS_ChannelIsActive(_currentStream)) 178 | { 179 | case BASSActive.BASS_ACTIVE_PAUSED: 180 | case BASSActive.BASS_ACTIVE_STOPPED: 181 | Playing = false; 182 | if ((int)ProcessWidth >= 9960) 183 | { 184 | PlayNext(); 185 | } 186 | break; 187 | case BASSActive.BASS_ACTIVE_STALLED: 188 | case BASSActive.BASS_ACTIVE_PLAYING: 189 | if (!Playing) 190 | { 191 | Playing = true; 192 | } 193 | var pos = Bass.BASS_ChannelGetPosition(_currentStream); 194 | CurrentPosition = Bass.BASS_ChannelBytes2Seconds(_currentStream, pos); 195 | ProcessWidth = Math.Ceiling(CurrentPosition * 10000.0 / TrackLength); 196 | break; 197 | } 198 | } 199 | 200 | private Task? _playTask; 201 | private CancellationTokenSource? _cancellationToken; 202 | private void PlaySong(SongResult? songResult) 203 | { 204 | if (songResult == null) 205 | { 206 | return; 207 | } 208 | if (string.IsNullOrEmpty(songResult.CopyUrl)) 209 | { 210 | return; 211 | } 212 | ProcessWidth = 0; 213 | CurrentPosition = 0; 214 | try 215 | { 216 | if (_playTask != null && _cancellationToken != null) 217 | { 218 | _cancellationToken.Cancel(); 219 | _playTask.Wait(1000); 220 | _playTask.Dispose(); 221 | _cancellationToken.Dispose(); 222 | } 223 | } 224 | catch (Exception) 225 | { 226 | // 227 | } 228 | _cancellationToken = new CancellationTokenSource(); 229 | if (_currentStream != 0) 230 | { 231 | Bass.BASS_ChannelStop(_currentStream); 232 | Bass.BASS_StreamFree(_currentStream); 233 | _currentStream = 0; 234 | } 235 | _playTask = Task.Factory.StartNew(() => 236 | { 237 | if (CurrentSong == null) 238 | { 239 | return; 240 | } 241 | var path = "https://ifish.fun" + CurrentSong.CopyUrl; 242 | using (var client = new HttpClient()) 243 | { 244 | var imgArr = client.GetByteArrayAsync(CurrentSong.PicUrl).Result; 245 | using (var ms = new MemoryStream(imgArr)) 246 | { 247 | if (AlbumPic != null) 248 | { 249 | AlbumPic.Dispose(); 250 | AlbumPic = null; 251 | } 252 | AlbumPic = new Bitmap(ms); 253 | } 254 | } 255 | if (path.ToLower().Contains("http")) 256 | { 257 | _currentStream = Bass.BASS_StreamCreateURL(path, 0, BASSFlag.BASS_DEFAULT | BASSFlag.BASS_STREAM_AUTOFREE | BASSFlag.BASS_MUSIC_AUTOFREE, _downloadProc, IntPtr.Zero); 258 | } 259 | else 260 | { 261 | _currentStream = Bass.BASS_StreamCreateFile(path, 0, 0, BASSFlag.BASS_DEFAULT | BASSFlag.BASS_STREAM_AUTOFREE | BASSFlag.BASS_MUSIC_AUTOFREE); 262 | } 263 | if (Bass.BASS_ChannelIsActive(_currentStream) != BASSActive.BASS_ACTIVE_PLAYING) 264 | { 265 | Bass.BASS_Start(); 266 | } 267 | if (_currentStream == 0) 268 | { 269 | PlayNext(); 270 | return; 271 | } 272 | if (Bass.BASS_ChannelPlay(_currentStream, true)) 273 | { 274 | TrackLength = Bass.BASS_ChannelBytes2Seconds(_currentStream, 275 | Bass.BASS_ChannelGetLength(_currentStream)); 276 | if (_updateTimer is {Enabled: false}) 277 | { 278 | _updateTimer.Start(); 279 | } 280 | return; 281 | } 282 | _currentStream = 0; 283 | Bass.BASS_Stop(); 284 | }, _cancellationToken.Token); 285 | } 286 | 287 | private void CachePlayingSong(IntPtr buffer, int length, IntPtr user) 288 | { 289 | // download finish 290 | if (buffer == IntPtr.Zero) 291 | { 292 | //save files 293 | Console.WriteLine("Cache Done!"); 294 | } 295 | else 296 | { 297 | //copy bytes 298 | //Marshal.Copy(buffer, new byte[length], 0 ,length); 299 | //Console.WriteLine(buffer.ToInt64()+"/"+length+"#"+user.ToInt64()); 300 | } 301 | } 302 | 303 | public void PlayNext() 304 | { 305 | if (TabIndex == 1) 306 | { 307 | RefreshList(); 308 | } 309 | else 310 | { 311 | PlayRandom(); 312 | } 313 | _playCount++; 314 | } 315 | 316 | public void PlayPrev() 317 | { 318 | PlayRandom(); 319 | } 320 | 321 | public void Play() 322 | { 323 | Bass.BASS_ChannelPlay(_currentStream, false); 324 | } 325 | 326 | public void Pause() 327 | { 328 | Bass.BASS_ChannelPause(_currentStream); 329 | } 330 | 331 | public void FreeBass() 332 | { 333 | if (_updateTimer != null) 334 | { 335 | _updateTimer.Stop(); 336 | _updateTimer.Dispose(); 337 | } 338 | if (_currentStream != 0) 339 | { 340 | Bass.BASS_ChannelStop(_currentStream); 341 | Bass.BASS_StreamFree(_currentStream); 342 | } 343 | Bass.BASS_Stop(); 344 | Bass.BASS_Free(); 345 | } 346 | 347 | public void LikeSong() 348 | { 349 | if (Liked || CurrentSong == null) 350 | { 351 | return; 352 | } 353 | DbHelper.LikeSong(CurrentSong); 354 | UpdateLiked(); 355 | } 356 | 357 | public void DislikeSong() 358 | { 359 | if (!Liked || CurrentSong == null) 360 | { 361 | return; 362 | } 363 | DbHelper.DislikeSong(CurrentSong); 364 | UpdateLiked(); 365 | } 366 | 367 | public void ShareSong() 368 | { 369 | if (CurrentSong == null) 370 | { 371 | return; 372 | } 373 | var text = CurrentSong.Name + " - " + CurrentSong.ArtistInfo[0].Name 374 | + " <" + CurrentSong.AlbumInfo.Name + ">"; 375 | var task = Application.Current.Clipboard.SetTextAsync(text); 376 | if (task.Status == TaskStatus.Created) 377 | { 378 | task.Start(); 379 | } 380 | task.Wait(); 381 | if (task.IsCompleted || task.IsCompletedSuccessfully) 382 | { 383 | SetTipText("歌曲信息已复制!"); 384 | } 385 | } 386 | 387 | private void SetTipText(string tip) 388 | { 389 | TipText = tip; 390 | Task.Delay(TimeSpan.FromSeconds(2)).ContinueWith((_ => 391 | { 392 | TipText = DefaultTip; 393 | })); 394 | } 395 | 396 | public void RefreshList() 397 | { 398 | SetTipText("列表更新中,请稍等..."); 399 | List songs; 400 | var type = "daily"; 401 | var date = DateTime.Now.ToString("yyyy-MM-dd"); 402 | switch (TabIndex) 403 | { 404 | case 0: 405 | songs = DbHelper.GetSongs(date, type); 406 | if (songs.Count <= 0) 407 | { 408 | var list = getSongsByType("xm"); 409 | if (list == null) 410 | { 411 | return; 412 | } 413 | songs = list; 414 | DbHelper.UpsertSongs(list, date, type); 415 | } 416 | if (_playCount % 10 == 0) 417 | { 418 | _isDiscoveryInit = true; 419 | Task.Factory.StartNew(() => 420 | { 421 | var list = getSongsByType("xm"); 422 | if (list == null) 423 | { 424 | return; 425 | } 426 | DbHelper.UpsertSongs(list, date, type); 427 | _isDiscoveryInit = false; 428 | }); 429 | } 430 | break; 431 | case 1: 432 | var lastSong = DbHelper.GetLastSong("init"); 433 | if (lastSong == null && !_isDiscoveryInit) 434 | { 435 | _isDiscoveryInit = true; 436 | Task.Factory.StartNew(() => 437 | { 438 | var list = getSongsByType("init"); 439 | if (list == null) 440 | { 441 | return; 442 | } 443 | DbHelper.UpsertSongs(list, date, "init"); 444 | _isDiscoveryInit = false; 445 | }); 446 | } 447 | songs = DbHelper.GetRandomSongs(); 448 | break; 449 | case 2: 450 | var ids = DbHelper.GetAllLikedSong(); 451 | songs = ids.Count > 0 ? DbHelper.GetSongsByIds(ids) : DbHelper.GetRandomSongs(); 452 | break; 453 | default: 454 | songs = DbHelper.GetRandomSongs(); 455 | break; 456 | } 457 | _songList = songs; 458 | PlayRandom(); 459 | } 460 | 461 | private List? getSongsByType(string type) 462 | { 463 | var url = "https://ifish.fun/api/music/daily?t="+type; 464 | using var client = new HttpClient(); 465 | var resp = client.GetAsync(url).Result; 466 | if (!resp.IsSuccessStatusCode) return null; 467 | var html = resp.Content.ReadAsStringAsync().Result; 468 | return string.IsNullOrEmpty(html) ? null : JsonConvert.DeserializeObject>(html); 469 | } 470 | } 471 | } -------------------------------------------------------------------------------- /FishFM/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace FishFM.ViewModels 4 | { 5 | public class ViewModelBase : ReactiveObject 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /FishFM/Views/CaptureWIndow.axaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /FishFM/Views/CaptureWIndow.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Controls; 4 | using Avalonia.Input; 5 | using Avalonia.Markup.Xaml; 6 | 7 | namespace FishFM.Views 8 | { 9 | public partial class CaptureWindow : Window 10 | { 11 | public CaptureWindow() 12 | { 13 | Width = Screens.Primary.Bounds.Width; 14 | Height = Screens.Primary.Bounds.Height; 15 | InitializeComponent(); 16 | #if DEBUG 17 | this.AttachDevTools(); 18 | #endif 19 | } 20 | 21 | private void InitializeComponent() 22 | { 23 | AvaloniaXamlLoader.Load(this); 24 | } 25 | 26 | private void InputElement_OnKeyDown(object? sender, KeyEventArgs e) 27 | { 28 | 29 | } 30 | 31 | private void TopLevel_OnOpened(object? sender, EventArgs e) 32 | { 33 | WindowState = WindowState.FullScreen; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /FishFM/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | avares://FishFM/Assets/#remixicon 27 | 28 | 29 | 30 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 65 | 68 | 71 | 74 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 90 | 93 | 96 | 98 | 99 | 100 | 101 | 102 | 103 | 107 | 111 | 112 | 115 | 119 | 123 | 126 | 129 | 132 | 135 | 138 | 139 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /FishFM/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using Avalonia; 4 | using Avalonia.Controls; 5 | using Avalonia.Interactivity; 6 | using Avalonia.Markup.Xaml; 7 | using FishFM.ViewModels; 8 | 9 | namespace FishFM.Views 10 | { 11 | public partial class MainWindow : Window 12 | { 13 | private MainWindowViewModel? _dataContext; 14 | 15 | public MainWindow() 16 | { 17 | InitializeComponent(); 18 | #if DEBUG 19 | this.AttachDevTools(); 20 | #endif 21 | } 22 | 23 | private void InitializeComponent() 24 | { 25 | AvaloniaXamlLoader.Load(this); 26 | } 27 | 28 | private void Window_OnClosing(object? sender, CancelEventArgs e) 29 | { 30 | _dataContext?.FreeBass(); 31 | } 32 | 33 | private void NextSong(object? sender, RoutedEventArgs e) 34 | { 35 | _dataContext?.PlayNext(); 36 | } 37 | 38 | private void PrevSong(object? sender, RoutedEventArgs e) 39 | { 40 | _dataContext?.PlayPrev(); 41 | } 42 | 43 | private void PlaySong(object? sender, RoutedEventArgs e) 44 | { 45 | _dataContext?.Play(); 46 | } 47 | 48 | private void PauseSong(object? sender, RoutedEventArgs e) 49 | { 50 | _dataContext?.Pause(); 51 | } 52 | 53 | private void TopLevel_OnOpened(object? sender, EventArgs e) 54 | { 55 | var ctx = DataContext; 56 | if (ctx is MainWindowViewModel model) 57 | { 58 | _dataContext = model; 59 | } 60 | } 61 | 62 | private void ShareSong(object? sender, RoutedEventArgs e) 63 | { 64 | _dataContext?.ShareSong(); 65 | } 66 | 67 | private void LikeSong(object? sender, RoutedEventArgs e) 68 | { 69 | _dataContext?.LikeSong(); 70 | } 71 | 72 | private void DislikeSong(object? sender, RoutedEventArgs e) 73 | { 74 | _dataContext?.DislikeSong(); 75 | } 76 | 77 | private void ChangeTab(object? sender, SelectionChangedEventArgs e) 78 | { 79 | if (sender is not TabControl tab) return; 80 | if (_dataContext == null) return; 81 | _dataContext.TabIndex = tab.SelectedIndex; 82 | _dataContext.RefreshList(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /FishFM/bass.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/FishFM/6a222554976034b00421d82a188914353e032698/FishFM/bass.dll -------------------------------------------------------------------------------- /FishFM/libbass.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/FishFM/6a222554976034b00421d82a188914353e032698/FishFM/libbass.dylib -------------------------------------------------------------------------------- /FishFM/libbass.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnyListen/FishFM/6a222554976034b00421d82a188914353e032698/FishFM/libbass.so -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FishFM 2 | A Cross Platform Music Discovery FM 3 | 4 | > 『鱼声FM』遇见属于你的声音 5 | 6 | 《鱼声FM》是一款以发现音乐为核心的跨平台音乐软件,音乐类型主要偏纯音乐、后摇滚、电子乐、国风音乐等,如果你喜欢主要面向喜欢「鱼声音乐精选」公众号推荐的音乐,那么这个软件会让你发现更多宝藏音乐。 7 | 8 | 9 | 目前仅支持PC端,支持Win、Linux、Mac,希望体验预览版的可以文末扫码入群。 10 | 11 | 下载链接: https://pan.baidu.com/s/18lu3-ltuZV0N1vzukbD5yg 提取码: updc 12 | 13 | ![Demo](https://img.ifish.fun/WX20220412-212126%402x.png) 14 | 15 | ## 自助编译 16 | ### macos 17 | 运行以下命令,然后在`项目目录/bin/Release/publish`下可以找到 `FishFM.app` 18 | ```shell 19 | ~/.dotnet/dotnet restore -r osx-x64 20 | ~/.dotnet/dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -property:Configuration=Release -p:UseAppHost=true 21 | ``` 22 | 23 | ### win-x86 24 | 运行以下命令,然后在`项目目录/bin/Release/publish`下可以找到 `FishFM.app` 25 | ```shell 26 | ~/.dotnet/dotnet restore -r win-x86 27 | ~/.dotnet/dotnet publish -r win-x86 -c Release --self-contained true -property:Configuration=Release -p:UseAppHost=true -p:DebugSymbols=false 28 | ``` 29 | 30 | --------------------------------------------------------------------------------