Skip to content

Commit e0caebe

Browse files
authored
Implement Soulseek support via Slskd
* Basic Slskd Indexer Support * Basic Slskd Download Provider * Refactor classes and fix issues
1 parent 026a837 commit e0caebe

32 files changed

+1415
-265
lines changed

.github/workflows/build.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ jobs:
3232
echo "Extracted version: $TAG_VERSION"
3333
echo "PACKAGE_VERSION=$TAG_VERSION" >> $GITHUB_ENV
3434
35+
# Log the extracted version
36+
echo "Version: $TAG_VERSION"
37+
38+
# Check if the version is a pre-release (starts with 0.)
39+
if [[ "$TAG_VERSION" == 0.* ]]; then
40+
echo "This is a pre-release version."
41+
echo "IS_PRERELEASE=true" >> $GITHUB_ENV
42+
else
43+
echo "This is a stable release."
44+
echo "IS_PRERELEASE=false" >> $GITHUB_ENV
45+
fi
46+
47+
- name: Log version and pre-release status
48+
run: |
49+
echo "Version: $PACKAGE_VERSION"
50+
echo "Is Pre-release: $IS_PRERELEASE"
51+
3552
- name: Extract repository name without owner
3653
id: extract_repo_name
3754
run: |
@@ -92,6 +109,7 @@ jobs:
92109
run: |
93110
echo "COMMIT_MESSAGES: $COMMIT_MESSAGES"
94111
echo "TAG_DESCRIPTION: $TAG_DESCRIPTION"
112+
echo "IS_PRERELEASE: $IS_PRERELEASE"
95113
96114
- name: Create GitHub Release and Upload Artifact
97115
uses: softprops/action-gh-release@v1
@@ -107,4 +125,4 @@ jobs:
107125
### 📦 **Artifact**
108126
The plugin artifact is attached to this release. Download it below!
109127
files: |
110-
${{ steps.find_plugin_dir.outputs.PLUGIN_OUTPUT_DIR }}/${{ env.PLUGIN_NAME }}-v${{ env.PACKAGE_VERSION }}.${{ matrix.framework }}.zip
128+
${{ steps.find_plugin_dir.outputs.PLUGIN_OUTPUT_DIR }}/${{ env.PLUGIN_NAME }}-v${{ env.PACKAGE_VERSION }}.${{ matrix.framework }}.zip

README.md

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
Here’s the complete README with the added short notice under the **Troubleshooting** section:
12
# Tubifarry for Lidarr 🎶
23
![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)
34

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. 🛠️
5+
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. 🛠️
56

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. 🎬🎵
7+
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. 🎬🎵
78

89
---
910

@@ -26,40 +27,39 @@ To switch to the Plugins Branch:
2627
---
2728

2829
### 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.
30+
- In Lidarr, go to `System -> Plugins`.
31+
- Paste `https://github.com/TypNull/Tubifarry` into the GitHub URL box and click **Install**.
4432

4533
---
4634

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.
35+
### Soulseek (Slskd) Setup 🎧
36+
Tubifarry supports **Slskd**, the Soulseek client, as both an **indexer** and **downloader**. Follow the steps below to configure it.
4937

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.
38+
#### **Setting Up the Soulseek Indexer**:
39+
1. Navigate to `Settings -> Indexers` and click **Add**.
40+
2. Select `Slskd` from the list of indexers.
41+
3. Configure the following settings:
42+
- **URL**: The URL of your Slskd instance (e.g., `http://localhost:5030`).
43+
- **API Key**: The API key for your Slskd instance (found in Slskd's settings under 'Options').
44+
- **Include Only Audio Files**: Enable to filter search results to audio files only (beta).
5645

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.
46+
#### **Setting Up the Soulseek Download Client**:
47+
1. Go to `Settings -> Download Clients` and click **Add**.
48+
2. Select `Slskd` from the list of download clients.
49+
3. Set the **download path** where downloaded files will be downloaded.
5950

6051
---
6152

62-
### Optional: FFmpeg and Audio Quality 🎧
53+
### YouTube Downloader Setup 🎥
54+
Tubifarry allows you to download music directly from YouTube. Follow the steps below to configure the YouTube downloader.
55+
56+
#### **Setting Up the YouTube Download Client**:
57+
1. Go to `Settings -> Download Clients` and click **Add**.
58+
2. Select `Youtube` from the list of download clients.
59+
3. Set the download path and adjust other settings as needed.
60+
4. **Optional**: If using FFmpeg, ensure the FFmpeg path is correctly configured.
61+
62+
#### **FFmpeg and Audio Conversion**:
6363
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.
6464

6565
**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,7 +76,23 @@ To enable this feature:
7676

7777
---
7878

79+
### Fetching Soundtracks from Sonarr and Radarr 🎬🎵
80+
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.
81+
82+
To enable this feature:
83+
1. **Set Up the Import List**:
84+
- Navigate to `Settings -> Import Lists` in Lidarr.
85+
- Add a new import list and select the option for **Arr-Soundtracks**.
86+
- Configure the settings to match your Sonarr and Radarr instances.
87+
- Provide a cache path to store responses from MusicBrainz for faster lookups.
88+
89+
2. **Enjoy Soundtracks**:
90+
- Once configured, Tubifarry will automatically fetch soundtracks from your Sonarr and Radarr libraries and add them to Lidarr for download and management.
91+
92+
---
93+
7994
### Troubleshooting 🛠️
95+
- **Slskd Download Path Permissions**: Ensure Lidarr has read/write access to the Slskd download path. Verify folder permissions and ensure the user running Lidarr has the necessary access. For Docker setups, confirm the volume is correctly mounted and permissions are set.
8096
- **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.
8197
- **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).
8298
- **No Release Found**: If no release is found, YouTube might flag the plugin as a bot (which it technically is). To avoid this and access higher-quality audio, you can log in using cookies.
@@ -90,7 +106,7 @@ To enable this feature:
90106
---
91107

92108
## 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. 🎉
109+
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. 🎉
94110

95111
---
96112

@@ -100,8 +116,8 @@ If you'd like to contribute to Tubifarry, feel free to open issues or submit pul
100116
---
101117

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

105121
---
106122

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

Tubifarry/Blocklisting/TubifarryBlocklist.cs renamed to Tubifarry/Blocklisting/BaseBlocklist.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
namespace NzbDrone.Core.Blocklisting
77
{
8-
public class TubifarryBlocklist : IBlocklistForProtocol
8+
public abstract class BaseBlocklist<TProtocol> : IBlocklistForProtocol where TProtocol : IDownloadProtocol
99
{
1010
private readonly IBlocklistRepository _blocklistRepository;
1111

12-
public TubifarryBlocklist(IBlocklistRepository blocklistRepository) => _blocklistRepository = blocklistRepository;
12+
public BaseBlocklist(IBlocklistRepository blocklistRepository) => _blocklistRepository = blocklistRepository;
1313

14-
public string Protocol => nameof(YoutubeDownloadProtocol);
14+
public string Protocol => nameof(TProtocol);
1515

16-
public bool IsBlocklisted(int artistId, ReleaseInfo release) => _blocklistRepository.BlocklistedByTorrentInfoHash(artistId, release.Guid).Any(b => SameRelease(b, release));
16+
public bool IsBlocklisted(int artistId, ReleaseInfo release) => _blocklistRepository.BlocklistedByTorrentInfoHash(artistId, release.Guid).Any(b => BaseBlocklist<TProtocol>.SameRelease(b, release));
1717

1818
public Blocklist GetBlocklist(DownloadFailedEvent message) => new()
1919
{
@@ -30,7 +30,6 @@ public class TubifarryBlocklist : IBlocklistForProtocol
3030
TorrentInfoHash = message.Data.GetValueOrDefault("guid")
3131
};
3232

33-
private bool SameRelease(Blocklist item, ReleaseInfo release) => release.Guid.IsNotNullOrWhiteSpace() ? release.Guid.Equals(item.TorrentInfoHash) : item.Indexer.Equals(release.Indexer, StringComparison.InvariantCultureIgnoreCase);
34-
33+
private static bool SameRelease(Blocklist item, ReleaseInfo release) => release.Guid.IsNotNullOrWhiteSpace() ? release.Guid.Equals(item.TorrentInfoHash) : item.Indexer.Equals(release.Indexer, StringComparison.InvariantCultureIgnoreCase);
3534
}
3635
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using NzbDrone.Core.Indexers;
2+
3+
namespace NzbDrone.Core.Blocklisting
4+
{
5+
public class YoutubeBlocklist : BaseBlocklist<YoutubeDownloadProtocol>
6+
{
7+
public YoutubeBlocklist(IBlocklistRepository blocklistRepository) : base(blocklistRepository) { }
8+
}
9+
10+
public class SoulseekBlocklist : BaseBlocklist<SoulseekDownloadProtocol>
11+
{
12+
public SoulseekBlocklist(IBlocklistRepository blocklistRepository) : base(blocklistRepository) { }
13+
}
14+
}

Tubifarry/Core/AlbumData.cs

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,58 @@ namespace Tubifarry.Core
99
/// </summary>
1010
public class AlbumData
1111
{
12+
public string IndexerName { get; }
1213
// Mixed
1314
public string AlbumId { get; set; } = string.Empty;
1415

1516
// Properties from AlbumInfo
1617
public string AlbumName { get; set; } = string.Empty;
1718
public string ArtistName { get; set; } = string.Empty;
18-
public string SpotifyUrl { get; set; } = string.Empty;
19+
public string InfoUrl { get; set; } = string.Empty;
1920
public string ReleaseDate { get; set; } = string.Empty;
2021
public DateTime ReleaseDateTime { get; set; }
2122
public string ReleaseDatePrecision { get; set; } = string.Empty;
2223
public int TotalTracks { get; set; }
2324
public bool ExplicitContent { get; set; }
24-
public string CoverUrl { get; set; } = string.Empty;
25+
public string CustomString { get; set; } = string.Empty;
2526
public string CoverResolution { get; set; } = string.Empty;
2627

27-
2828
// Properties from YoutubeSearchResults
2929
public int Bitrate { get; set; }
3030
public long Duration { get; set; }
3131

32-
//Not used
32+
// Soulseek
33+
public long? Size { get; set; }
34+
public int Priotity { get; set; }
35+
36+
// Not used
3337
public AudioFormat Codec { get; set; } = AudioFormat.AAC;
3438

39+
public AlbumData(string name) => IndexerName = name;
40+
3541
/// <summary>
3642
/// Converts AlbumData into a ReleaseInfo object.
3743
/// </summary>
3844
public ReleaseInfo ToReleaseInfo() => new()
3945
{
40-
Guid = $"Tubifarry-{AlbumId}-{Bitrate}",
46+
Guid = $"{IndexerName}-{AlbumId}-{Bitrate}",
4147
Artist = ArtistName,
4248
Album = AlbumName,
4349
DownloadUrl = AlbumId,
44-
InfoUrl = SpotifyUrl,
45-
PublishDate = ReleaseDateTime,
50+
InfoUrl = InfoUrl,
51+
PublishDate = ReleaseDateTime == DateTime.MinValue ? DateTime.UtcNow : ReleaseDateTime,
4652
DownloadProtocol = nameof(YoutubeDownloadProtocol),
4753
Title = ConstructTitle(),
4854
Codec = Codec.ToString(),
4955
Resolution = CoverResolution,
50-
Source = CoverUrl,
56+
Source = CustomString,
5157
Container = Bitrate.ToString(),
52-
Size = (Duration > 0 ? Duration : TotalTracks * 300) * Bitrate * 1000 / 8
58+
Size = Size ?? (Duration > 0 ? Duration : TotalTracks * 300) * Bitrate * 1000 / 8
5359
};
5460

61+
/// <summary>
62+
/// Parses the release date based on the precision.
63+
/// </summary>
5564
public void ParseReleaseDate() => ReleaseDateTime = ReleaseDatePrecision switch
5665
{
5766
"year" => new DateTime(int.Parse(ReleaseDate), 1, 1),
@@ -67,19 +76,23 @@ public class AlbumData
6776
private string ConstructTitle()
6877
{
6978
string normalizedAlbumName = NormalizeAlbumName(AlbumName);
70-
// Start with the basic format: Artist - Album
79+
7180
string title = $"{ArtistName} - {normalizedAlbumName}";
7281

73-
// Add the release year if available
74-
if (ReleaseDateTime.Year > 0)
82+
if (ReleaseDateTime != DateTime.MinValue)
7583
title += $" - {ReleaseDateTime.Year}";
7684

77-
// Add the explicit content indicator if applicable
7885
if (ExplicitContent)
7986
title += " [Explicit]";
8087

81-
// Add the bitrate and source type
82-
title += $" [{Codec} {Bitrate}kbps] [WEB]";
88+
int calculatedBitrate = Bitrate;
89+
if (calculatedBitrate <= 0 && Size.HasValue && Duration > 0)
90+
calculatedBitrate = (int)((Size.Value * 8) / (Duration * 1000));
91+
92+
if (AudioFormatHelper.IsLossyFormat(Codec))
93+
title += $" [{Codec} {calculatedBitrate}kbps] [WEB]";
94+
else
95+
title += $" [{Codec}] [WEB]";
8396

8497
return title;
8598
}
@@ -89,21 +102,16 @@ private string ConstructTitle()
89102
/// </summary>
90103
/// <param name="albumName">The album name to normalize.</param>
91104
/// <returns>The normalized album name.</returns>
92-
private string NormalizeAlbumName(string albumName)
105+
private static string NormalizeAlbumName(string albumName)
93106
{
94-
// Handle featuring artists (e.g., "feat.", "ft.", "Feat.", "Ft.", etc.)
95107
Regex featRegex = new(@"(?i)\b(feat\.|ft\.|featuring)\b", RegexOptions.IgnoreCase);
96108
if (featRegex.IsMatch(albumName))
97109
{
98-
// Extract the featuring artist(s)
99110
Match match = featRegex.Match(albumName);
100111
string featuringArtist = albumName.Substring(match.Index + match.Length).Trim();
101112

102-
// Format the featuring artist(s) in a consistent way
103113
albumName = $"{albumName.Substring(0, match.Index).Trim()} (feat. {featuringArtist})";
104114
}
105-
106-
// Replace content inside parentheses (except for featuring artists) with curly braces
107115
albumName = Regex.Replace(albumName, @"\((?!feat\.)[^)]*\)", match => $"{{{match.Value.Trim('(', ')')}}}");
108116

109117
return albumName;

0 commit comments

Comments
 (0)