├── .gitignore ├── Assets ├── logo.png ├── logo.xcf └── meta.json ├── Jellyfin.Plugin.FinTube ├── Configuration │ ├── PluginConfiguration.cs │ └── configPage.html ├── Jellyfin.Plugin.FinTube.csproj ├── Plugin.cs ├── Api │ └── FinTubeActivityController.cs └── Pages │ └── downloadPage.html ├── README.MD └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | Jellyfin.Plugin.FinTube/bin 2 | Jellyfin.Plugin.FinTube/obj -------------------------------------------------------------------------------- /Assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AECX/FinTube/HEAD/Assets/logo.png -------------------------------------------------------------------------------- /Assets/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AECX/FinTube/HEAD/Assets/logo.xcf -------------------------------------------------------------------------------- /Assets/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": "General", 3 | "changelog": "Initial", 4 | "description": "", 5 | "guid": "d20aa9ed-eafc-4578-b320-4e3b7093129c", 6 | "name": "TubeFin", 7 | "overview": "Easily add YouTube Media", 8 | "owner": "AECX", 9 | "targetAbi": "10.8.1.0", 10 | "timestamp": "2023-07-25T12:47:28.0000000Z", 11 | "version": "1.0.1.0", 12 | "status": "Active", 13 | "autoUpdate": false, 14 | "imagePath": "/var/lib/jellyfin/plugins/TubeFin/logo.png" 15 | } 16 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.FinTube/Configuration/PluginConfiguration.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Plugins; 2 | 3 | namespace Jellyfin.Plugin.FinTube.Configuration; 4 | 5 | /// 6 | /// Plugin configuration. 7 | /// 8 | public class PluginConfiguration : BasePluginConfiguration 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | public PluginConfiguration() 14 | { 15 | exec_YTDL = "/usr/local/bin/yt-dlp"; 16 | exec_ID3 = "/usr/bin/id3v2"; 17 | } 18 | 19 | /// 20 | /// Executable for youtube-dl/youtube-dlp 21 | /// 22 | public string exec_YTDL { get; set; } 23 | 24 | /// 25 | /// Executable for ID3v2 26 | /// 27 | public string exec_ID3 { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.FinTube/Jellyfin.Plugin.FinTube.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Jellyfin.Plugin.FinTube 6 | 1.1.0.0 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # FinTube 2 | 3 | Easily add content from YouTube to your Jellyfin installation 4 | 5 | ![](https://raw.githubusercontent.com/AECX/FinTube/master/Assets/logo.png) 6 | 7 | ## Dependencies 8 | 9 | FinTube requires [YouTube-DL/YouTube-DLP](https://github.com/yt-dlp/yt-dlp) and [id3v2](https://sourceforge.net/projects/id3v2/) for full functionality, however YT-DLP is sufficient for operation. 10 | 11 | Check the link above to install YT-DLP and install id3v2: 12 | 13 | - Debian/Ubuntu `# sudo apt install id3v2` 14 | - Arch `# sudo pacman -S id3v2` 15 | 16 | For other OS please check your package manager. 17 | 18 | ## Install 19 | 20 | ### Add my Repository 21 | 22 | 1. In your Admin Dashboard navigate to "Plugins" 23 | 2. Switch to the "Repositories" tab 24 | 3. Click "+" and add the Repository `https://raw.githubusercontent.com/AECX/FinTube/master/manifest.json` 25 | Name it "HenkeGG" or "AECX" - Or whatever helps you remember 26 | 27 | ### Install and configure the plugin 28 | 29 | 1. Switch to the "Catalog" tab 30 | 2. Search for the "FinTube" plugin and click install 31 | 3. Restart the Server and head back to the "Plugins" Sections 32 | 4. Click on FinTube and Select "Settings", enter a valid executable for yt-dlp/youtube-dl 33 | 5. Optionally: Enter a valid executable for id3v2 to be able to Tag Music with Artist, Title, Album and Track information 34 | 35 | Now you are ready to go, head to the "FinTube" plugin page (at the bottom of your dashboard navigation), enter information as desired to start importing from YouTube. 36 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.FinTube/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using Jellyfin.Plugin.FinTube.Configuration; 5 | using MediaBrowser.Common.Configuration; 6 | using MediaBrowser.Common.Plugins; 7 | using MediaBrowser.Model.Plugins; 8 | using MediaBrowser.Model.Serialization; 9 | 10 | namespace Jellyfin.Plugin.FinTube; 11 | 12 | /// 13 | /// The main plugin. 14 | /// 15 | public class Plugin : BasePlugin, IHasWebPages 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// Instance of the interface. 21 | /// Instance of the interface. 22 | public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) 23 | : base(applicationPaths, xmlSerializer) 24 | { 25 | Instance = this; 26 | 27 | ConfigurationChanged += OnConfigurationChanged; 28 | } 29 | 30 | /// 31 | public override string Name => "FinTube"; 32 | 33 | /// 34 | public override Guid Id => Guid.Parse("d20aa9ed-eafc-4578-b320-4e3b7093129c"); 35 | 36 | /// 37 | /// Gets the current plugin instance. 38 | /// 39 | public static Plugin? Instance { get; private set; } 40 | public PluginConfiguration PluginConfiguration => Configuration; 41 | 42 | /// 43 | public IEnumerable GetPages() 44 | { 45 | return new[] 46 | { 47 | new PluginPageInfo 48 | { 49 | Name = this.Name, 50 | EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace) 51 | }, 52 | 53 | new PluginPageInfo 54 | { 55 | Name = @"FinTubeDownload", 56 | EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.downloadPage.html", GetType().Namespace), 57 | EnableInMainMenu = true 58 | } 59 | }; 60 | } 61 | 62 | private void OnConfigurationChanged(object? sender, BasePluginConfiguration e) 63 | { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "guid": "d20aa9ed-eafc-4578-b320-4e3b7093129c", 4 | "name": "FinTube", 5 | "overview": "Import Videos/Music from YouTube", 6 | "description": "Lets you download YT videos as mp4 or mp3 with support for id3v2 tags. Requires yt-dlp/youtube-dl and (optionally) id3v2.", 7 | "owner": "Maurice 'AECX' Henke", 8 | "category": "General", 9 | "imageUrl": "https://raw.githubusercontent.com/AECX/FinTube/master/Assets/logo.png", 10 | "versions": [ 11 | { 12 | "version": "1.1.0.0", 13 | "changelog": "Added possibility to provide a custom filename", 14 | "targetAbi": "10.8.10.0", 15 | "sourceUrl": "https://github.com/AECX/FinTube/releases/download/v1.1.0.0/FinTube-1.1.0.0.zip", 16 | "checksum": "61a936d047cbaf23935a8686a1626834", 17 | "timestamp": "2025-09-23T07:13:30Z" 18 | }, 19 | { 20 | "version": "1.0.1.1", 21 | "changelog": "Fixed Version mismatch and libraries showing on DL page", 22 | "targetAbi": "10.8.10.0", 23 | "sourceUrl": "https://github.com/AECX/FinTube/releases/download/v1.0.1.1/FinTube-1.0.1.1.zip", 24 | "checksum": "8d109651a763267083323c8e6390549f", 25 | "timestamp": "2025-09-10T07:00:00Z" 26 | }, 27 | { 28 | "version": "1.0.1.0", 29 | "changelog": "Initial Release", 30 | "targetAbi": "10.8.10.0", 31 | "sourceUrl": "https://github.com/AECX/FinTube/releases/download/v1.0.1/FinTube-1.0.1.zip", 32 | "checksum": "20d1786dedf3213ad371edcbafea934d", 33 | "timestamp": "2024-05-12T11:40:00Z" 34 | } 35 | ] 36 | }, 37 | { 38 | "guid": "d20aa9ed-eafc-4578-b320-4e3b7093129c", 39 | "name": "FinTube", 40 | "overview": "Import Videos/Music from YouTube", 41 | "description": "Lets you download YT videos as mp4 or mp3 with support for id3v2 tags. Requires yt-dlp/youtube-dl and (optionally) id3v2.", 42 | "owner": "Maurice 'AECX' Henke", 43 | "category": "General", 44 | "imageUrl": "https://raw.githubusercontent.com/AECX/FinTube/master/Assets/logo.png", 45 | "versions": [ 46 | { 47 | "version": "1.0.0.0", 48 | "changelog": "Initial Release", 49 | "targetAbi": "10.8.10.0", 50 | "sourceUrl": "https://github.com/AECX/FinTube/releases/download/v1.0/FinTube-1.0.zip", 51 | "checksum": "02eeb5390d5e2a93a241c986bbde6d60", 52 | "timestamp": "2023-08-23T22:36:00Z" 53 | } 54 | ] 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.FinTube/Configuration/configPage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FinTube 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 | 14 | 15 |
The executable filepath to youtube-dl/yt-dlp
16 |
17 |
18 | 19 | 20 |
The executable filepath to id3v2
21 |
22 |
23 | 26 |
27 |
28 |
29 |
30 | 60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.FinTube/Api/FinTubeActivityController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Mime; 7 | using Jellyfin.Data.Entities; 8 | using Jellyfin.Plugin.FinTube.Configuration; 9 | using MediaBrowser.Controller.Configuration; 10 | using MediaBrowser.Controller.Library; 11 | using MediaBrowser.Model.IO; 12 | using Microsoft.AspNetCore.Authorization; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.AspNetCore.Mvc; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace Jellyfin.Plugin.FinTube.Api; 18 | 19 | [ApiController] 20 | [Authorize(Roles = "Administrator")] 21 | [Route("fintube")] 22 | [Produces(MediaTypeNames.Application.Json)] 23 | public class FinTubeActivityController : ControllerBase 24 | { 25 | private readonly ILogger _logger; 26 | private readonly ILoggerFactory _loggerFactory; 27 | private readonly IFileSystem _fileSystem; 28 | private readonly IServerConfigurationManager _config; 29 | private readonly IUserManager _userManager; 30 | private readonly ILibraryManager _libraryManager; 31 | 32 | public FinTubeActivityController( 33 | ILoggerFactory loggerFactory, 34 | IFileSystem fileSystem, 35 | IServerConfigurationManager config, 36 | IUserManager userManager, 37 | ILibraryManager libraryManager) 38 | { 39 | _loggerFactory = loggerFactory; 40 | _logger = loggerFactory.CreateLogger(); 41 | _fileSystem = fileSystem; 42 | _config = config; 43 | _userManager = userManager; 44 | _libraryManager = libraryManager; 45 | 46 | _logger.LogInformation("FinTubeActivityController Loaded"); 47 | } 48 | 49 | public class FinTubeData 50 | { 51 | public string ytid {get; set;} = ""; 52 | public string targetlibrary{get; set;} = ""; 53 | public string targetfolder{get; set;} = ""; 54 | public string targetfilename { get; set; } = ""; 55 | public bool audioonly{get; set;} = false; 56 | public bool preferfreeformat{get; set;} = false; 57 | public string videoresolution{get; set;} = ""; 58 | public string artist{get; set;} = ""; 59 | public string album{get; set;} = ""; 60 | public string title{get; set;} = ""; 61 | public int track{get; set;} = 0; 62 | 63 | } 64 | 65 | [HttpPost("submit_dl")] 66 | [ProducesResponseType(StatusCodes.Status200OK)] 67 | public ActionResult> FinTubeDownload([FromBody] FinTubeData data) 68 | { 69 | try 70 | { 71 | _logger.LogInformation("FinTubeDownload : {ytid} to {targetfoldeer}, prefer free format: {preferfreeformat} audio only: {audioonly}", data.ytid, data.targetfolder, data.preferfreeformat, data.audioonly); 72 | 73 | Dictionary response = new Dictionary(); 74 | PluginConfiguration? config = Plugin.Instance.Configuration; 75 | String status = ""; 76 | 77 | 78 | // check binaries 79 | if(!System.IO.File.Exists(config.exec_YTDL)) 80 | throw new Exception("YT-DL Executable configured incorrectly"); 81 | 82 | bool hasid3v2 = System.IO.File.Exists(config.exec_ID3); 83 | 84 | 85 | // Ensure proper / separator 86 | data.targetfolder = String.Join("/", data.targetfolder.Split("/", StringSplitOptions.RemoveEmptyEntries)); 87 | String targetPath = data.targetlibrary.EndsWith("/") ? data.targetlibrary + data.targetfolder : data.targetlibrary + "/" + data.targetfolder; 88 | // Create Folder if it doesn't exist 89 | if(!System.IO.Directory.CreateDirectory(targetPath).Exists) 90 | throw new Exception("Directory could not be created"); 91 | 92 | 93 | // Check for tags 94 | bool hasTags = 1 < (data.title.Length + data.album.Length + data.artist.Length + data.track.ToString().Length); 95 | 96 | // Save file with ytdlp as mp4 or mp3 depending on audioonly 97 | String targetFilename; 98 | String targetExtension = (data.preferfreeformat ? (data.audioonly ? @".opus" : @".webm") : (data.audioonly ? @".mp3" : @".mp4")); 99 | 100 | if (!String.IsNullOrWhiteSpace(data.targetfilename)) 101 | targetFilename = System.IO.Path.Combine(targetPath, $"{data.targetfilename}"); 102 | else if (data.audioonly && hasTags && data.title.Length > 1) // Use title Tag for filename 103 | targetFilename = System.IO.Path.Combine(targetPath, $"{data.title}"); 104 | else // Use YTID as filename 105 | targetFilename = System.IO.Path.Combine(targetPath, $"{data.ytid}"); 106 | 107 | // Check if filename exists 108 | if(System.IO.File.Exists(targetFilename)) 109 | throw new Exception($"File {targetFilename} already exists"); 110 | 111 | status += $"Filename: {targetFilename}
"; 112 | 113 | String args; 114 | if(data.audioonly) 115 | { 116 | args = "-x"; 117 | if(data.preferfreeformat) 118 | args += " --prefer-free-format"; 119 | else 120 | args += " --audio-format mp3"; 121 | args += $" -o \"{targetFilename}.%(ext)s\" {data.ytid}"; 122 | } 123 | else 124 | { 125 | if(data.preferfreeformat) 126 | args = "--prefer-free-format"; 127 | else 128 | args = "-f mp4"; 129 | if(!string.IsNullOrEmpty(data.videoresolution)) 130 | args += $" -S res:{data.videoresolution}"; 131 | args += $" -o \"{targetFilename}-%(title)s.%(ext)s\" {data.ytid}"; 132 | } 133 | 134 | status += $"Exec: {config.exec_YTDL} {args}
"; 135 | 136 | var procyt = createProcess(config.exec_YTDL, args); 137 | procyt.Start(); 138 | procyt.WaitForExit(); 139 | 140 | // If audioonly AND id3v2 AND tags are set - Tag the mp3 file 141 | if (data.audioonly && hasid3v2 && hasTags) 142 | { 143 | args = $"-a \"{data.artist}\" -A \"{data.album}\" -t \"{data.title}\" -T \"{data.track}\" \"{targetFilename}{targetExtension}\""; 144 | 145 | status += $"Exec: {config.exec_ID3} {args}
"; 146 | 147 | var procid3 = createProcess(config.exec_ID3, args); 148 | procid3.Start(); 149 | procid3.WaitForExit(); 150 | } 151 | 152 | status += "File Saved!"; 153 | 154 | response.Add("message", status); 155 | return Ok(response); 156 | } 157 | catch(Exception e) 158 | { 159 | _logger.LogError(e, e.Message); 160 | return StatusCode(500, new Dictionary() {{"message", e.Message}}); 161 | } 162 | } 163 | 164 | [HttpGet("libraries")] 165 | [ProducesResponseType(StatusCodes.Status200OK)] 166 | public ActionResult> FinTubeLibraries() 167 | { 168 | try 169 | { 170 | _logger.LogInformation("FinTubeDLibraries count: {count}", _libraryManager.GetVirtualFolders().Count); 171 | 172 | Dictionary response = new Dictionary(); 173 | response.Add("data", _libraryManager.GetVirtualFolders().Select(i => i.Locations).ToArray()); 174 | return Ok(response); 175 | } 176 | catch(Exception e) 177 | { 178 | _logger.LogError(e, e.Message); 179 | return StatusCode(500, new Dictionary() {{"message", e.Message}}); 180 | } 181 | } 182 | 183 | private static Process createProcess(String exe, String args) 184 | { 185 | ProcessStartInfo startInfo = new ProcessStartInfo() { FileName = exe, Arguments = args }; 186 | return new Process() { StartInfo = startInfo }; 187 | } 188 | } -------------------------------------------------------------------------------- /Jellyfin.Plugin.FinTube/Pages/downloadPage.html: -------------------------------------------------------------------------------- 1 |
3 |
4 |
5 |

FinTube Download

6 | Make sure to configure FinTube first. 7 | 8 |
9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 22 |
Base library destination path to download file
23 |
24 | 25 |
26 | 27 | 28 |
Your Media file will be stored here assuming JellyFin has Write Permissions, this will also recursively create the folder(s) required
Relative path from target library (optional)
29 |
30 | 31 |
32 | 33 | 34 |
This is the filename if you want to override the yt-dl(p) default (optional)
35 |
36 | 37 |
38 | 42 |
Will usually download audio as .opus and videos as .webm
43 |
44 | 45 |
46 | 50 |
Check this if you want to store as mp3, otherwise the file is saved in mp4
51 |
52 | 53 |
54 |
55 | 56 | 66 |
Maximum desired resolution if available. Set to default if you want to download the best available.
67 |
68 |
69 | 70 | 94 | 95 | 98 | 99 |
100 | 101 |
102 | 103 |
104 | 105 | 214 |
215 |
216 |
217 |
--------------------------------------------------------------------------------