├── .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 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------