Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
b7ce46e
Fix error if title has umlauts and no subs are available
PCJones Jan 31, 2025
c2057db
Fix temporary files paths
PCJones Jan 31, 2025
929b8c1
Don't use fallback by id if show name is short
PCJones Jan 31, 2025
f23a66e
Fix ß not being renamed for mkvmerge
PCJones Feb 3, 2025
15a0cca
Fix outdated logging message
PCJones Feb 21, 2025
33995f4
Fetch german title from UmlautAdaptarr API
PCJones Feb 26, 2025
f732508
Add support for GERMAN.SUBBED
PCJones Feb 26, 2025
ba82f3e
Fix burned in sub detection
PCJones Feb 26, 2025
4194997
Fix docker entrypoint if UID/GID already exists
PCJones Mar 5, 2025
2d01dfe
Add error message to logger
PCJones Mar 5, 2025
96e6ac0
Fix user agent not being set
PCJones Mar 19, 2025
adb5166
Fix wrong summary
PCJones Mar 25, 2025
ebb6538
Fix subtitle download
PCJones Apr 1, 2025
c8dab83
Fix typo
PCJones Apr 1, 2025
0bee3d1
Fix line endings
PCJones Apr 17, 2025
7bd511a
Add error messages to wizard controller
PCJones Apr 27, 2025
2ebde84
Make categories configurable
PCJones Apr 27, 2025
10aa4fd
Always remove brackets from search
PCJones Apr 27, 2025
330c13e
Add mediathekarr.json to .gitnignore
PCJones Apr 27, 2025
efa742a
Remove blackhole default categories
PCJones Apr 27, 2025
325c6a7
Ensure category directories are created
PCJones Apr 27, 2025
0f62035
Update README.md
PCJones Apr 27, 2025
1ffe406
Update README.md
PCJones Apr 27, 2025
e7a9581
Create Dockerfile.arm64
PCJones Apr 27, 2025
70c4ce2
Merge pull request #45 from PCJones/main
PCJones Apr 27, 2025
013a9c2
Accept text and xml in headers, fixes subtitle download failure
PCJones Apr 29, 2025
15f4f08
Fix wizard bugs
PCJones Apr 29, 2025
be3a124
Update README.md
PCJones Apr 29, 2025
83d7fb0
Update README.md
PCJones Apr 29, 2025
ea2ac86
Update README.md
PCJones Apr 29, 2025
0675dde
Handle invalid dates in parser
PCJones Apr 30, 2025
7ed6bce
Merge branch 'v2' of https://github.com/PCJones/MediathekArr into v2
PCJones Apr 30, 2025
df8ac43
Improve language detection
PCJones May 8, 2025
d22cde4
Make TvdbEpisode name nullable
PCJones Jun 17, 2025
878b4bd
Handle bad TVDB responses
PCJones Jun 23, 2025
15d93b8
Add TODO message
PCJones Jun 23, 2025
e94c0e5
Add support for the Homarr SABnzbd integration … (#41)
MrAn0nym Jun 26, 2025
d99eba6
Fix AbsoluteEpisodeNumber matching strategy
PCJones Jul 23, 2025
35ed701
Merge branch 'v2' of https://github.com/PCJones/MediathekArr into v2
PCJones Jul 23, 2025
eddba97
Namespace Refactoring (#53)
ChrisonSimtian Jul 24, 2025
9655c88
Fix invalid removal of unmatched items if there was a ruleset for tha…
PCJones Jul 24, 2025
d4b1d80
Merge branch 'v2' of https://github.com/PCJones/MediathekArr into v2
PCJones Jul 24, 2025
3ce8628
Add missing logger
PCJones Jul 24, 2025
140a0cd
Add missing logger
PCJones Jul 24, 2025
c80fdfc
Fix LowerThan MatchType
PCJones Jul 24, 2025
8868c2b
Fix assuming mkvmerge failure on exit code 1
PCJones Aug 3, 2025
8a3032d
Fix arm64 Dockerfile
gregorsp Sep 14, 2025
62e6723
Fix wizard vor Prowlarr v2
PCJones Sep 20, 2025
c99f524
Merge branch 'v2' of https://github.com/PCJones/MediathekArr into v2
PCJones Sep 20, 2025
0785b41
Add support for adding fake nzbs via url
PCJones Sep 20, 2025
00cf9a4
Search by ruleset topics and show name with parallel API calls
PCJones Jan 13, 2026
714ed6f
Fix #56: Add middle dot to TitleRegexSymbols
PCJones Jan 17, 2026
2c87cd4
Add MaxParallelDownloads setting
PCJones Jan 17, 2026
df6451e
Update invalid chars
PCJones Feb 21, 2026
96f54c0
Base64 encode links/titles in fake nzb
PCJones Feb 21, 2026
3f84b94
Add M3U download support (#71)
a-serafini Feb 21, 2026
86652c9
Refactor SubtitleConverter; Add support for ORF subtitles (#76)
PCJones Feb 21, 2026
48f5546
Add support for ORF/KIKA subtitles
PCJones Feb 21, 2026
f5bd04d
Add fake m3u8 size to fallback handler
PCJones Feb 21, 2026
2051b35
Add pagination to mediathekviewweb API
PCJones May 12, 2026
5d576df
Initialize tvdb_id_cache table in database
PCJones May 12, 2026
133d4b3
Add tvdbid to error message
PCJones Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ FodyWeavers.xsd

# MediathekArr specifics
config.json
mediathekarr.json
*.mp4
*.mkv
ffmpeg/
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ RUN apt-get update && apt-get install -y \
gosu \
wget \
gnupg \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*

# Add MKVToolNix repository and install
Expand Down
53 changes: 53 additions & 0 deletions Dockerfile.arm64
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
FROM --platform=linux/arm64 mcr.microsoft.com/dotnet/sdk:9.0 AS build-env
WORKDIR /app

# Copy and restore dependencies
COPY . ./
RUN dotnet restore

# Publish MediathekArrServer
WORKDIR /app/MediathekArrServer
RUN dotnet publish -c Release -o /app/out/MediathekArrServer

# Publish MediathekArrDownloader
WORKDIR /app/MediathekArr
RUN dotnet publish -c Release -o /app/out/MediathekArrDownloader

# Final runtime image
FROM --platform=linux/arm64 mcr.microsoft.com/dotnet/aspnet:9.0

# Install required packages
RUN apt-get update && apt-get install -y \
tar \
xz-utils \
gosu \
wget \
gnupg \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*

# Add MKVToolNix repository and install
RUN wget -O /etc/apt/keyrings/gpg-pub-moritzbunkus.gpg https://mkvtoolnix.download/gpg-pub-moritzbunkus.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/gpg-pub-moritzbunkus.gpg] https://mkvtoolnix.download/debian/ bookworm main" > /etc/apt/sources.list.d/mkvtoolnix.download.list && \
apt-get update && apt-get install -y \
mkvtoolnix

# Set working directory
WORKDIR /app

# Copy the built apps from the build environment
COPY --from=build-env /app/out/MediathekArrServer /app/MediathekArrServer
COPY --from=build-env /app/out/MediathekArrDownloader /app/MediathekArrDownloader

# Copy the startup script
COPY docker_start.sh /app/docker_start.sh
RUN chmod +x /app/docker_start.sh

# Create required directories
RUN mkdir -p /data/mediathek/incomplete /data/mediathek/complete

ENV ASPNETCORE_ENVIRONMENT=Production
ENV CONFIG_PATH=/app/config

# Use the shell script to start both processes
ENTRYPOINT ["/app/docker_start.sh"]
157 changes: 157 additions & 0 deletions MediathekArr/Controllers/ApiProxyControllerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net;

namespace MediathekArr.Controllers;

public abstract class ApiProxyControllerBase : ControllerBase
{
protected readonly IHttpClientFactory HttpClientFactory;

protected ApiProxyControllerBase(IHttpClientFactory httpClientFactory)
{
HttpClientFactory = httpClientFactory;
}

protected IActionResult? ValidateRequiredParameters(string apiKey, string host, string hostName = "Host")
{
if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(host))
{
return BadRequest($"{hostName} and API key are required.");
}
return null;
}

protected HttpClient CreateHttpClient(string apiKey)
{
var httpClient = HttpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Add("X-Api-Key", apiKey);
return httpClient;
}

protected async Task<string> ReadRequestBodyAsync()
{
using var reader = new StreamReader(Request.Body);
return await reader.ReadToEndAsync();
}

protected IActionResult HandleErrorResponse(HttpStatusCode statusCode, string responseContent, string errorContext)
{
string message = $"Error {errorContext}";

if (!string.IsNullOrWhiteSpace(responseContent))
{
try
{
// Try to parse as array of validation errors
var errorArray = JsonConvert.DeserializeObject<List<JObject>>(responseContent);
if (errorArray?.Count > 0)
{
var errorMessages = errorArray
.Select(err => (string)err["errorMessage"])
.Where(msg => !string.IsNullOrEmpty(msg))
.ToList();

if (errorMessages.Count != 0)
{
message = string.Join(", ", errorMessages);
var propertyName = errorArray.Select(err => (string)err["propertyName"]).FirstOrDefault();
var attemptedValue = errorArray.Select(err => (string)err["attemptedValue"]).FirstOrDefault();
if (propertyName is not null)
{
message += $":{Environment.NewLine}Property Name: \"{propertyName}\"{Environment.NewLine}Attempted Value: \"{attemptedValue}\"";
}
}
}
else
{
// Try to parse as a single error object with errorMessage property
var errorObj = JsonConvert.DeserializeObject<JObject>(responseContent);
if (errorObj?["errorMessage"] != null)
{
message = errorObj["errorMessage"].ToString();
}
}
}
catch
{
// If parsing fails, fall back to raw response content
message = responseContent;
}
}

var errorResult = new
{
error = new
{
message
}
};

return StatusCode((int)statusCode, errorResult);
}


protected async Task<IActionResult> ExecuteApiRequest(
string apiKey,
string host,
string endpoint,
HttpMethod method,
string errorContext,
Func<string, IActionResult> processSuccessResponse = null,
string hostName = "Host",
bool isProwlarr = false)
{
var validationResult = ValidateRequiredParameters(apiKey, host, hostName);
if (validationResult != null)
return validationResult;

var cleanedHostName = host.TrimEnd('/');
var httpClient = CreateHttpClient(apiKey);

try
{
StringContent content = null;

if (method != HttpMethod.Get)
{
var rawBody = await ReadRequestBodyAsync();
content = new StringContent(rawBody, System.Text.Encoding.UTF8, "application/json");
}

HttpResponseMessage response;

if (method == HttpMethod.Get)
{
response = await httpClient.GetAsync($"{cleanedHostName}/{endpoint}");
}
else if (method == HttpMethod.Post)
{
response = await httpClient.PostAsync($"{cleanedHostName}/{endpoint}", content);
}
else // PUT
{
response = await httpClient.PutAsync($"{cleanedHostName}/{endpoint}", content);
}

var responseContent = await response.Content.ReadAsStringAsync();

if (!response.IsSuccessStatusCode)
{
return HandleErrorResponse(response.StatusCode, responseContent, errorContext);
}

if (processSuccessResponse != null)
{
return processSuccessResponse(responseContent);
}

return Content(responseContent, "application/json");
}
catch (Exception ex)
{
return StatusCode(500, $"Error {errorContext}: {ex.Message}");
}
}
}
49 changes: 44 additions & 5 deletions MediathekArr/Controllers/DownloadController.Config.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using MediathekArrDownloader.Models;
using MediathekArr.Models;
using Microsoft.AspNetCore.Mvc;

namespace MediathekArrDownloader.Controllers;
namespace MediathekArr.Controllers;

public partial class DownloadController
{
Expand All @@ -10,14 +10,27 @@ public IActionResult GetConfig()
{
var incompleteEnv = Environment.GetEnvironmentVariable("DOWNLOAD_INCOMPLETE_PATH");
var completeEnv = Environment.GetEnvironmentVariable("DOWNLOAD_COMPLETE_PATH");

var categoriesEnv = Environment.GetEnvironmentVariable("CATEGORIES");
var maxParallelDownloadsEnv = Environment.GetEnvironmentVariable("MAX_PARALLEL_DOWNLOADS");

// Parse categories from environment variable if present
if (!string.IsNullOrEmpty(categoriesEnv))
{
_config.Categories = categoriesEnv.Split(',')
.Select(c => c.Trim())
.Where(c => !string.IsNullOrEmpty(c))
.ToArray();
}

var configDetails = new
{
config = _config,
overrides = new
{
IncompletePath = !string.IsNullOrEmpty(incompleteEnv),
CompletePath = !string.IsNullOrEmpty(completeEnv)
CompletePath = !string.IsNullOrEmpty(completeEnv),
Categories = !string.IsNullOrEmpty(categoriesEnv),
MaxParallelDownloads = !string.IsNullOrEmpty(maxParallelDownloadsEnv)
}
};

Expand All @@ -29,6 +42,10 @@ public IActionResult UpdateConfig([FromBody] Config newConfig)
{
var incompleteEnv = Environment.GetEnvironmentVariable("DOWNLOAD_INCOMPLETE_PATH");
var completeEnv = Environment.GetEnvironmentVariable("DOWNLOAD_COMPLETE_PATH");
var categoriesEnv = Environment.GetEnvironmentVariable("CATEGORIES");
var maxParallelDownloadsEnv = Environment.GetEnvironmentVariable("MAX_PARALLEL_DOWNLOADS");

bool requiresRestart = false;

// Prevent updates to fields overridden by environment variables
if (string.IsNullOrEmpty(incompleteEnv))
Expand All @@ -41,6 +58,27 @@ public IActionResult UpdateConfig([FromBody] Config newConfig)
_config.CompletePath = newConfig.CompletePath;
}

// Only update categories if not overridden by environment variable
if (string.IsNullOrEmpty(categoriesEnv))
{
if (_config.Categories != newConfig.Categories)
{
_config.Categories = newConfig.Categories;

// Create directories for each category
_downloadService.InitializeCompleteDirectories();
}
}

if (string.IsNullOrEmpty(maxParallelDownloadsEnv))
{
if (_config.MaxParallelDownloads != newConfig.MaxParallelDownloads)
{
_config.MaxParallelDownloads = Math.Clamp(newConfig.MaxParallelDownloads, 1, 10);
requiresRestart = true;
}
}

// Persist updated config to file
var configPathEnv = Environment.GetEnvironmentVariable("CONFIG_PATH");

Expand All @@ -59,9 +97,10 @@ public IActionResult UpdateConfig([FromBody] Config newConfig)
}
System.IO.File.WriteAllText(configFilePath, System.Text.Json.JsonSerializer.Serialize(_config, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));

return Ok(new { status = "success" });
return Ok(new { status = "success", requiresRestart });
}


[HttpGet("browse")]
public IActionResult BrowsePath([FromQuery] string path = "")
{
Expand Down
Loading