Skip to content

Commit 98809b3

Browse files
author
Meyn
committed
Refactor classes and fix issues
1 parent 91f0b11 commit 98809b3

File tree

13 files changed

+366
-183
lines changed

13 files changed

+366
-183
lines changed

README.md

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Tubifarry for Lidarr 🎶
22
![Downloads](https://img.shields.io/github/downloads/TypNull/Tubifarry/total) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/TypNull/Tubifarry) ![GitHub last commit](https://img.shields.io/github/last-commit/TypNull/Tubifarry) ![License](https://img.shields.io/github/license/TypNull/Tubifarry) ![GitHub stars](https://img.shields.io/github/stars/TypNull/Tubifarry)
33

4-
Tubifarry is a plugin for **Lidarr** that fetches metadata from **Spotify** and **YouTube**, enabling direct music downloads from YouTube. Built on the foundation of trevTV's projects, it leverages the YouTube API for seamless integration. 🛠️
4+
Tubifarry is a versatile plugin for **Lidarr** that enhances your music library by fetching metadata from **Spotify** and enabling direct music downloads from **YouTube**. While it is not explicitly a Spotify-to-YouTube downloader, it leverages the YouTube API to seamlessly integrate music downloads into your Lidarr setup. Built on the foundation of trevTV's projects, Tubifarry also supports **Slskd**, the Soulseek client, as both an **indexer** and **downloader**, allowing you to tap into the vast music collection available on the Soulseek network. 🛠️
55

6-
Additionally, Tubifarry supports fetching soundtracks from **Sonarr** (series) and **Radarr** (movies) and adding them to Lidarr using the **Arr-Soundtracks** import list feature. This allows you to easily manage and download soundtracks for your favorite movies and TV shows. 🎬🎵
6+
Additionally, Tubifarry supports fetching soundtracks from **Sonarr** (series) and **Radarr** (movies) and adding them to Lidarr using the **Arr-Soundtracks** import list feature. This makes it easy to manage and download soundtracks for your favorite movies and TV shows. 🎬🎵
77

88
---
99

@@ -26,40 +26,39 @@ To switch to the Plugins Branch:
2626
---
2727

2828
### Plugin Installation 📥
29-
30-
#### **For Docker Users**:
31-
1. **Install the Plugin**:
32-
- In Lidarr, go to `System -> Plugins`.
33-
- Paste `https://github.com/TypNull/Tubifarry` into the GitHub URL box and click **Install**.
34-
35-
2. **Configure the Indexer**:
36-
- Navigate to `Settings -> Indexers` and click **Add**.
37-
- In the modal, select `Tubifarry` (located under **Other** at the bottom).
38-
39-
3. **Set Up the Download Client**:
40-
- Go to `Settings -> Download Clients` and click **Add**.
41-
- In the modal, choose `Youtube` (under **Other** at the bottom).
42-
- Set the download path and adjust other settings as needed.
43-
- **Optional**: If using FFmpeg, ensure the FFmpeg path is correctly configured.
29+
- In Lidarr, go to `System -> Plugins`.
30+
- Paste `https://github.com/TypNull/Tubifarry` into the GitHub URL box and click **Install**.
4431

4532
---
4633

47-
### Fetching Soundtracks from Sonarr and Radarr 🎬🎵
48-
Tubifarry also supports fetching soundtracks from **Sonarr** (for TV series) and **Radarr** (for movies) and adding them to Lidarr using the **Arr-Soundtracks** import list feature. This allows you to easily manage and download soundtracks for your favorite movies and TV shows.
34+
### Soulseek (Slskd) Setup 🎧
35+
Tubifarry supports **Slskd**, the Soulseek client, as both an **indexer** and **downloader**. Follow the steps below to configure it.
4936

50-
To enable this feature:
51-
1. **Set Up the Import List**:
52-
- Navigate to `Settings -> Import Lists` in Lidarr.
53-
- Add a new import list and select the option for **Arr-Soundtracks**.
54-
- Configure the settings to match your Sonarr and Radarr instances.
55-
- Provide a cache path to store responses from MusicBrainz for faster lookups.
37+
#### **Setting Up the Soulseek Indexer**:
38+
1. Navigate to `Settings -> Indexers` and click **Add**.
39+
2. Select `Slskd` from the list of indexers.
40+
3. Configure the following settings:
41+
- **URL**: The URL of your Slskd instance (e.g., `http://localhost:5030`).
42+
- **API Key**: The API key for your Slskd instance (found in Slskd's settings under 'Options').
43+
- **Include Only Audio Files**: Enable to filter search results to audio files only (beta).
5644

57-
2. **Enjoy Soundtracks**:
58-
- Once configured, Tubifarry will automatically fetch soundtracks from your Sonarr and Radarr libraries and add them to Lidarr for download and management.
45+
#### **Setting Up the Soulseek Download Client**:
46+
1. Go to `Settings -> Download Clients` and click **Add**.
47+
2. Select `Slskd` from the list of download clients.
48+
3. Set the **download path** where downloaded files will be downloaded.
5949

6050
---
6151

62-
### Optional: FFmpeg and Audio Quality 🎧
52+
### YouTube Downloader Setup 🎥
53+
Tubifarry allows you to download music directly from YouTube. Follow the steps below to configure the YouTube downloader.
54+
55+
#### **Setting Up the YouTube Download Client**:
56+
1. Go to `Settings -> Download Clients` and click **Add**.
57+
2. Select `Youtube` from the list of download clients.
58+
3. Set the download path and adjust other settings as needed.
59+
4. **Optional**: If using FFmpeg, ensure the FFmpeg path is correctly configured.
60+
61+
#### **FFmpeg and Audio Conversion**:
6362
1. **FFmpeg**: FFmpeg can be used to extract audio from downloaded files, which are typically embedded in MP4 containers. If you choose to use FFmpeg, ensure it is installed and accessible in your system's PATH or the specified FFmpeg path. If not, the plugin does attempt to download it automatically during setup. Without FFmpeg, songs will be downloaded in their original format, which may not require additional processing.
6463

6564
**Important Note**: If FFmpeg is not used, Lidarr may incorrectly interpret the MP4 container as corrupt. While FFmpeg usage is **recommended**, it is not strictly necessary. However, to avoid potential issues, you can choose to extract audio without re-encoding, but this may lead to better compatibility with Lidarr.
@@ -76,6 +75,21 @@ To enable this feature:
7675

7776
---
7877

78+
### Fetching Soundtracks from Sonarr and Radarr 🎬🎵
79+
Tubifarry also supports fetching soundtracks from **Sonarr** (for TV series) and **Radarr** (for movies) and adding them to Lidarr using the **Arr-Soundtracks** import list feature. This allows you to easily manage and download soundtracks for your favorite movies and TV shows.
80+
81+
To enable this feature:
82+
1. **Set Up the Import List**:
83+
- Navigate to `Settings -> Import Lists` in Lidarr.
84+
- Add a new import list and select the option for **Arr-Soundtracks**.
85+
- Configure the settings to match your Sonarr and Radarr instances.
86+
- Provide a cache path to store responses from MusicBrainz for faster lookups.
87+
88+
2. **Enjoy Soundtracks**:
89+
- Once configured, Tubifarry will automatically fetch soundtracks from your Sonarr and Radarr libraries and add them to Lidarr for download and management.
90+
91+
---
92+
7993
### Troubleshooting 🛠️
8094
- **Optional: FFmpeg Issues**: If you choose to use FFmpeg and songs fail to process, verify that FFmpeg is correctly installed and accessible in your system's PATH. If not, try reinstalling or downloading it manually.
8195
- **Metadata Issues**: If metadata is not being added to downloaded files, confirm that the files are in a supported format. If using FFmpeg, ensure it is extracting audio to formats like AAC embedded in MP4 containers (check debug logs).
@@ -90,7 +104,7 @@ To enable this feature:
90104
---
91105

92106
## Acknowledgments 🙌
93-
Special thanks to **trevTV** for laying the groundwork with [Lidarr.Plugin.Tidal](https://github.com/TrevTV/Lidarr.Plugin.Tidal), [Lidarr.Plugin.Deezer](https://github.com/TrevTV/Lidarr.Plugin.Deezer), and [Lidarr.Plugin.Qobuz](https://github.com/TrevTV/Lidarr.Plugin.Qobuz). Additionally, thanks to [IcySnex/YouTubeMusicAPI](https://github.com/IcySnex/YouTubeMusicAPI) for providing the YouTube API. 🎉
107+
Special thanks to [**trevTV**](https://github.com/TrevTV) for laying the groundwork with his plugins. Additionally, thanks to [**IcySnex**](https://github.com/IcySnex) for providing the YouTube API. 🎉
94108

95109
---
96110

@@ -100,8 +114,8 @@ If you'd like to contribute to Tubifarry, feel free to open issues or submit pul
100114
---
101115

102116
## License 📄
103-
Tubifarry is licensed under the MIT License. See the [LICENSE](https://github.com/TypNull/Tubifarry/blob/main/LICENSE) file for more details.
117+
Tubifarry is licensed under the MIT License. See the [LICENSE](https://github.com/TypNull/Tubifarry/blob/master/LICENSE) file for more details.
104118

105119
---
106120

107-
Enjoy seamless music downloads with Tubifarry! 🎧
121+
Enjoy seamless music downloads with Tubifarry! 🎧

Tubifarry/Core/AlbumData.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class AlbumData
3131

3232
// Soulseek
3333
public long? Size { get; set; }
34+
public int Priotity { get; set; }
3435

3536
// Not used
3637
public AudioFormat Codec { get; set; } = AudioFormat.AAC;
@@ -47,7 +48,7 @@ public class AlbumData
4748
Album = AlbumName,
4849
DownloadUrl = AlbumId,
4950
InfoUrl = InfoUrl,
50-
PublishDate = ReleaseDateTime,
51+
PublishDate = ReleaseDateTime == DateTime.MinValue ? DateTime.UtcNow : ReleaseDateTime,
5152
DownloadProtocol = nameof(YoutubeDownloadProtocol),
5253
Title = ConstructTitle(),
5354
Codec = Codec.ToString(),

Tubifarry/Core/AudioFormat.cs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@ public enum AudioFormat
2222
internal static class AudioFormatHelper
2323
{
2424
private static readonly AudioFormat[] _lossyFormats = new[] {
25-
AudioFormat.AAC,
26-
AudioFormat.MP3,
27-
AudioFormat.Opus,
28-
AudioFormat.Vorbis,
29-
AudioFormat.MP4,
30-
AudioFormat.AMR,
31-
AudioFormat.WMA };
25+
AudioFormat.AAC,
26+
AudioFormat.MP3,
27+
AudioFormat.Opus,
28+
AudioFormat.Vorbis,
29+
AudioFormat.MP4,
30+
AudioFormat.AMR,
31+
AudioFormat.WMA
32+
};
3233

3334
/// <summary>
3435
/// Returns the correct file extension for a given audio codec.
@@ -46,9 +47,6 @@ internal static class AudioFormatHelper
4647
_ => ".aac" // Default to AAC if the codec is unknown
4748
};
4849

49-
public static bool IsLossyFormat(AudioFormat format) => _lossyFormats.Contains(format);
50-
51-
5250
/// <summary>
5351
/// Determines the audio format from a given codec string.
5452
/// </summary>
@@ -79,7 +77,13 @@ internal static class AudioFormatHelper
7977
AudioFormat.Vorbis => ".ogg",
8078
AudioFormat.FLAC => ".flac",
8179
AudioFormat.WAV => ".wav",
82-
_ => ".aac"
80+
AudioFormat.AIFF => ".aiff",
81+
AudioFormat.MIDI => ".midi",
82+
AudioFormat.AMR => ".amr",
83+
AudioFormat.WMA => ".wma",
84+
AudioFormat.MP4 => ".mp4",
85+
AudioFormat.OGG => ".ogg",
86+
_ => ".aac" // Default to AAC if the format is unknown
8387
};
8488

8589
/// <summary>
@@ -93,5 +97,29 @@ internal static class AudioFormatHelper
9397
ReEncodeOptions.Vorbis => AudioFormat.Vorbis,
9498
_ => AudioFormat.Unknown
9599
};
100+
101+
/// <summary>
102+
/// Determines if a given format is lossy.
103+
/// </summary>
104+
public static bool IsLossyFormat(AudioFormat format) => _lossyFormats.Contains(format);
105+
106+
/// <summary>
107+
/// Determines the audio format from a given file extension.
108+
/// </summary>
109+
public static AudioFormat GetAudioCodecFromExtension(string extension) => extension?.ToLowerInvariant().TrimStart('.') switch
110+
{
111+
// Common file extensions
112+
"m4a" or "mp4" or "aac" => AudioFormat.AAC,
113+
"mp3" => AudioFormat.MP3,
114+
"opus" => AudioFormat.Opus,
115+
"ogg" or "vorbis" => AudioFormat.Vorbis,
116+
"flac" or "alac" => AudioFormat.FLAC,
117+
"wav" => AudioFormat.WAV,
118+
"aiff" or "aif" or "aifc" => AudioFormat.AIFF,
119+
"mid" or "midi" => AudioFormat.MIDI,
120+
"amr" => AudioFormat.AMR,
121+
"wma" => AudioFormat.WMA,
122+
_ => AudioFormat.Unknown // Default for unknown extensions
123+
};
96124
}
97125
}

Tubifarry/Download/Clients/Soulseek/SlskdClient.cs

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,35 @@ namespace NzbDrone.Core.Download.Clients.Soulseek
1515
public class SlskdClient : DownloadClientBase<SlskdProviderSettings>
1616
{
1717
private readonly IHttpClient _httpClient;
18-
private Dictionary<string, SlskdDownloadItem> _downloadMapping;
19-
private string _downloadPath = string.Empty;
20-
private bool _isLocalhost = false;
21-
22-
public SlskdClient(IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, Logger logger)
23-
: base(configService, diskProvider, remotePathMappingService, logger)
24-
{
25-
_httpClient = httpClient;
26-
_downloadMapping = new Dictionary<string, SlskdDownloadItem>();
27-
}
18+
private static readonly Dictionary<DownloadKey, SlskdDownloadItem> _downloadMappings = new();
2819

2920
public override string Name => "Slskd";
3021

3122
public override string Protocol => nameof(SoulseekDownloadProtocol);
3223

24+
public SlskdClient(IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, Logger logger)
25+
: base(configService, diskProvider, remotePathMappingService, logger) => _httpClient = httpClient;
26+
27+
3328
public override async Task<string> Download(RemoteAlbum remoteAlbum, IIndexer indexer)
3429
{
35-
SlskdDownloadItem item = new(Guid.NewGuid().ToString(), remoteAlbum);
30+
SlskdDownloadItem item = new(remoteAlbum);
3631
HttpRequest request = BuildHttpRequest(remoteAlbum.Release.DownloadUrl, HttpMethod.Post, remoteAlbum.Release.Source);
3732
HttpResponse response = await _httpClient.ExecuteAsync(request);
3833

3934
if (response.StatusCode != HttpStatusCode.Created)
4035
throw new DownloadClientException("Failed to create download.");
41-
42-
_downloadMapping[item.ID] = item;
43-
return item.ID;
36+
AddDownloadItem(item);
37+
return item.ID.ToString();
4438
}
4539

40+
4641
public override IEnumerable<DownloadClientItem> GetItems()
4742
{
4843
UpdateDownloadItemsAsync().Wait();
4944
DownloadClientItemClientInfo clientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
50-
foreach (KeyValuePair<string, SlskdDownloadItem> kpv in _downloadMapping)
45+
foreach (DownloadClientItem? clientItem in GetDownloadItems().Select(x => x.GetDownloadClientItem(Settings.DownloadPath)))
5146
{
52-
DownloadClientItem clientItem = kpv.Value.GetDownloadClientItem(_downloadPath);
5347
clientItem.DownloadClientInfo = clientInfo;
5448
yield return clientItem;
5549
}
@@ -58,10 +52,10 @@ public override IEnumerable<DownloadClientItem> GetItems()
5852
public override void RemoveItem(DownloadClientItem clientItem, bool deleteData)
5953
{
6054
if (!deleteData) return;
61-
_downloadMapping.TryGetValue(clientItem.DownloadId, out SlskdDownloadItem? slskdItem);
55+
SlskdDownloadItem? slskdItem = GetDownloadItem(clientItem.DownloadId);
6256
if (slskdItem == null) return;
6357
RemoveItemAsync(slskdItem).Wait();
64-
_downloadMapping.Remove(clientItem.DownloadId);
58+
RemoveDownloadItem(clientItem.DownloadId);
6559
}
6660

6761
private async Task UpdateDownloadItemsAsync()
@@ -78,7 +72,10 @@ private async Task UpdateDownloadItemsAsync()
7872
IEnumerable<SlskdDownloadDirectory> data = SlskdDownloadDirectory.GetDirectories(directoriesElement);
7973
foreach (SlskdDownloadDirectory dir in data)
8074
{
81-
SlskdDownloadItem? item = _downloadMapping.Values.FirstOrDefault(x => x.FileData.Any(y => y.Filename?.Contains(dir.Directory!) ?? false));
75+
HashCode hash = new();
76+
foreach (SlskdDownloadFile file in dir.Files ?? new List<SlskdDownloadFile>())
77+
hash.Add(file.Filename);
78+
SlskdDownloadItem? item = GetDownloadItem(hash.ToHashCode());
8279
if (item == null)
8380
continue;
8481
item.Username ??= user.GetProperty("username").GetString()!;
@@ -114,16 +111,18 @@ private async Task UpdateDownloadItemsAsync()
114111
return null;
115112
}
116113

117-
public override DownloadClientInfo GetStatus()
114+
public override DownloadClientInfo GetStatus() => new()
118115
{
119-
if (string.IsNullOrEmpty(_downloadPath))
120-
_downloadPath = string.IsNullOrEmpty(Settings.DownloadPath) ? FetchDownloadPathAsync().Result ?? Settings.DownloadPath : Settings.DownloadPath;
121-
return new DownloadClientInfo
122-
{
123-
IsLocalhost = _isLocalhost,
124-
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.BaseUrl, new OsPath(_downloadPath)) }
125-
};
126-
}
116+
IsLocalhost = Settings.IsLocalhost,
117+
OutputRootFolders = new List<OsPath> { Settings.IsRemotePath ? _remotePathMappingService.RemapRemoteToLocal(Settings.BaseUrl, new OsPath(Settings.DownloadPath)) : new OsPath(Settings.DownloadPath) }
118+
};
119+
120+
private SlskdDownloadItem? GetDownloadItem(string downloadId) => GetDownloadItem(int.Parse(downloadId));
121+
private SlskdDownloadItem? GetDownloadItem(int downloadId) => _downloadMappings.TryGetValue(new DownloadKey(Definition.Id, downloadId), out SlskdDownloadItem? item) ? item : null;
122+
private IEnumerable<SlskdDownloadItem> GetDownloadItems() => _downloadMappings.Where(kvp => kvp.Key.OuterKey == Definition.Id).Select(kvp => kvp.Value);
123+
private void AddDownloadItem(SlskdDownloadItem item) => _downloadMappings[new DownloadKey(Definition.Id, item.ID)] = item;
124+
private bool RemoveDownloadItem(string downloadId) => _downloadMappings.Remove(new DownloadKey(Definition.Id, int.Parse(downloadId)));
125+
127126

128127
protected override void Test(List<ValidationFailure> failures) => failures.AddIfNotNull(TestConnection().Result);
129128

@@ -134,7 +133,7 @@ protected async Task<ValidationFailure> TestConnection()
134133
Uri uri = new(Settings.BaseUrl);
135134
if (uri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
136135
IPAddress.TryParse(uri.Host, out IPAddress? ipAddress) && IPAddress.IsLoopback(ipAddress))
137-
_isLocalhost = true;
136+
Settings.IsLocalhost = true;
138137
}
139138
catch (UriFormatException ex)
140139
{
@@ -164,10 +163,12 @@ protected async Task<ValidationFailure> TestConnection()
164163
if (string.IsNullOrEmpty(serverState) || !serverState.Contains("Connected"))
165164
return new ValidationFailure("BaseUrl", $"Slskd server is not connected. State: {serverState}");
166165

167-
168-
_downloadPath = string.IsNullOrEmpty(Settings.DownloadPath) ? await FetchDownloadPathAsync() ?? Settings.DownloadPath : Settings.DownloadPath;
169-
if (string.IsNullOrEmpty(_downloadPath))
170-
return new ValidationFailure("DownloadPath", "DownloadPath could not be found or is invalid.");
166+
if (string.IsNullOrEmpty(Settings.DownloadPath))
167+
{
168+
Settings.DownloadPath = await FetchDownloadPathAsync() ?? string.Empty;
169+
if (string.IsNullOrEmpty(Settings.DownloadPath))
170+
return new ValidationFailure("DownloadPath", "DownloadPath could not be found or is invalid.");
171+
}
171172
return null!;
172173
}
173174
catch (HttpException ex)

0 commit comments

Comments
 (0)