Skip to content
This repository has been archived by the owner on Oct 13, 2024. It is now read-only.

feat: track theme file md5 hash #185

Merged
merged 1 commit into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
149 changes: 140 additions & 9 deletions Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,44 @@ private List<string> FixtureThemerrDbUrls()
return youtubeUrls;
}

[Fact]
[Trait("Category", "Unit")]
private void TestGetExistingThemerrDataValue()
{
string themerrDataPath;
themerrDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"dummy.json");

// ensure correct values are returned
Assert.Equal(
"dummy_value",
_themerrManager.GetExistingThemerrDataValue("dummy_key", themerrDataPath));
Assert.Equal(
"https://www.youtube.com/watch?v=E8nxMWr2sr4",
_themerrManager.GetExistingThemerrDataValue("youtube_theme_url", themerrDataPath));

// ensure null when the key does not exist
Assert.Null(_themerrManager.GetExistingThemerrDataValue("invalid_key", themerrDataPath));

// ensure null when the file does not exist
themerrDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"no_file.json");

Assert.Null(_themerrManager.GetExistingThemerrDataValue("any_key", themerrDataPath));

// test empty json file
themerrDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"empty.json");

Assert.Null(_themerrManager.GetExistingThemerrDataValue("any_key", themerrDataPath));
}

[Fact]
[Trait("Category", "Unit")]
private void TestSaveMp3()
Expand Down Expand Up @@ -180,15 +218,80 @@ private void TestSaveMp3InvalidUrl()

[Fact]
[Trait("Category", "Unit")]
private void TestShouldSkipDownload()
private void TestContinueDownload()
{
var themePath = Path.Combine(
"theme.mp3");
var themerrDataPath = Path.Combine(
"themerr_data.json");

var shouldSkipDownload = _themerrManager.ShouldSkipDownload(themePath, themerrDataPath);
Assert.False(shouldSkipDownload, "ShouldSkipDownload returned True");
string themePath;
string themerrDataPath;

// test when neither theme nor data file exists
themePath = Path.Combine(
"no_file.mp3");
themerrDataPath = Path.Combine(
"no_file.json");
Assert.True(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned False");

// test when theme does not exist and data file does
themePath = Path.Combine(
"no_file.mp3");

// copy the dummy.json to a secondary location
var ogFile = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"dummy.json");
themerrDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"dummy2.json");

// copy the dummy.json
File.Copy(ogFile, themerrDataPath);
Assert.True(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned False");
Assert.False(File.Exists(themerrDataPath), $"File {themerrDataPath} was not removed");

// test when theme file exists but data file does not
themePath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"audio_stub.mp3");
themerrDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"no_file.json");
Assert.False(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned True");

// test when both theme and data file exist, but hash is empty in data file
themePath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"audio_stub.mp3");
themerrDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"dummy.json");
Assert.True(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned False");

// test when both theme and data file exist, and md5 hashes match
themePath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"audio_stub.mp3");
themerrDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"audio_themerr_data.json");
Assert.True(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned False");

// test when both theme and data file exist, and md5 hashes do not match
themePath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"audio_stub.mp3");
themerrDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"audio_themerr_data_user_overwritten.json");
Assert.False(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned True");
}

[Fact]
Expand Down Expand Up @@ -273,11 +376,16 @@ private void TestSaveThemerrData()
// set mock themerrDataPath using a random number
var mockThemerrDataPath = $"themerr_{new Random().Next()}.json";

var stubVideoPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"video_stub.mp4");

// loop over each themerrDbLink
foreach (var youtubeThemeUrl in FixtureYoutubeUrls())
{
// save themerr data
var fileExists = _themerrManager.SaveThemerrData(mockThemerrDataPath, youtubeThemeUrl);
var fileExists = _themerrManager.SaveThemerrData(stubVideoPath, mockThemerrDataPath, youtubeThemeUrl);
Assert.True(fileExists, $"SaveThemerrData did not return True for {youtubeThemeUrl}");

// check if file exists
Expand All @@ -293,4 +401,27 @@ private void TestSaveThemerrData()
$"youtubeThemeUrl {youtubeThemeUrl} does not match savedYoutubeThemeUrl {savedYoutubeThemeUrl}");
}
}

[Fact]
[Trait("Category", "Unit")]
private void TestGetMd5Hash()
{
var stubVideoPath = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"video_stub.mp4");

var expectedMd5HashFile = Path.Combine(
Directory.GetCurrentDirectory(),
"data",
"video_stub.mp4.md5");

// get expected md5 hash out of file
var expectedMd5Hash = File.ReadAllText(expectedMd5HashFile).Trim();

// get actual md5 hash
var actualMd5Hash = _themerrManager.GetMd5Hash(stubVideoPath);

Assert.Equal(expectedMd5Hash, actualMd5Hash);
}
}
Binary file added Jellyfin.Plugin.Themerr.Tests/data/audio_stub.mp3
Binary file not shown.
3 changes: 3 additions & 0 deletions Jellyfin.Plugin.Themerr.Tests/data/audio_themerr_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"theme_md5": "44c5eaa73e9b362911aaaf12a5609ab7"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"theme_md5": "abcd"
}
5 changes: 5 additions & 0 deletions Jellyfin.Plugin.Themerr.Tests/data/dummy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"downloaded_timestamp": "2023-12-12T04:07:41.7659077Z",
"youtube_theme_url": "https://www.youtube.com/watch?v=E8nxMWr2sr4",
"dummy_key": "dummy_value"
}
1 change: 1 addition & 0 deletions Jellyfin.Plugin.Themerr.Tests/data/empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Binary file not shown.
93 changes: 77 additions & 16 deletions Jellyfin.Plugin.Themerr/ThemerrManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,21 @@
}

/// <summary>
/// Get the existing youtube theme url from the themerr data file if it exists.
/// Get a value from the themerr data file if it exists.
/// </summary>
/// <param name="key">The key to search for.</param>
/// <param name="themerrDataPath">The path to the themerr data file.</param>
/// <returns>The existing YouTube theme url if it exists, empty string otherwise.</returns>
public static string GetExistingYoutubeThemeUrl(string themerrDataPath)
/// <returns>The value of the key if it exists, null otherwise.</returns>
public string GetExistingThemerrDataValue(string key, string themerrDataPath)
{
if (!System.IO.File.Exists(themerrDataPath))
{
return string.Empty;
return null;
}

var jsonString = System.IO.File.ReadAllText(themerrDataPath);
dynamic jsonData = JsonConvert.DeserializeObject(jsonString);
return jsonData?.youtube_theme_url;
return jsonData?[key];
}

/// <summary>
Expand Down Expand Up @@ -132,42 +133,83 @@
var themePath = GetThemePath(movie);
var themerrDataPath = GetThemerrDataPath(movie);

if (ShouldSkipDownload(themePath, themerrDataPath))
if (!ContinueDownload(themePath, themerrDataPath))
{
return;
}

var existingYoutubeThemeUrl = GetExistingYoutubeThemeUrl(themerrDataPath);
var existingYoutubeThemeUrl = GetExistingThemerrDataValue("youtube_theme_url", themerrDataPath);

Check warning on line 141 in Jellyfin.Plugin.Themerr/ThemerrManager.cs

View check run for this annotation

Codecov / codecov/patch

Jellyfin.Plugin.Themerr/ThemerrManager.cs#L141

Added line #L141 was not covered by tests

// get tmdb id
var tmdbId = movie.GetProviderId(MetadataProvider.Tmdb);

// create themerrdb url
var themerrDbLink = CreateThemerrDbLink(tmdbId);
var themerrDbUrl = CreateThemerrDbLink(tmdbId);

Check warning on line 147 in Jellyfin.Plugin.Themerr/ThemerrManager.cs

View check run for this annotation

Codecov / codecov/patch

Jellyfin.Plugin.Themerr/ThemerrManager.cs#L147

Added line #L147 was not covered by tests

var youtubeThemeUrl = GetYoutubeThemeUrl(themerrDbLink, movieTitle);
var youtubeThemeUrl = GetYoutubeThemeUrl(themerrDbUrl, movieTitle);

Check warning on line 149 in Jellyfin.Plugin.Themerr/ThemerrManager.cs

View check run for this annotation

Codecov / codecov/patch

Jellyfin.Plugin.Themerr/ThemerrManager.cs#L149

Added line #L149 was not covered by tests

if (string.IsNullOrEmpty(youtubeThemeUrl) || youtubeThemeUrl == existingYoutubeThemeUrl)
{
return;
}

SaveMp3(themePath, youtubeThemeUrl);
SaveThemerrData(themerrDataPath, youtubeThemeUrl);
var successMp3 = SaveMp3(themePath, youtubeThemeUrl);

Check warning on line 156 in Jellyfin.Plugin.Themerr/ThemerrManager.cs

View check run for this annotation

Codecov / codecov/patch

Jellyfin.Plugin.Themerr/ThemerrManager.cs#L156

Added line #L156 was not covered by tests
if (!successMp3)
{
return;

Check warning on line 159 in Jellyfin.Plugin.Themerr/ThemerrManager.cs

View check run for this annotation

Codecov / codecov/patch

Jellyfin.Plugin.Themerr/ThemerrManager.cs#L158-L159

Added lines #L158 - L159 were not covered by tests
}

var successThemerrData = SaveThemerrData(themePath, themerrDataPath, youtubeThemeUrl);

Check warning on line 162 in Jellyfin.Plugin.Themerr/ThemerrManager.cs

View check run for this annotation

Codecov / codecov/patch

Jellyfin.Plugin.Themerr/ThemerrManager.cs#L162

Added line #L162 was not covered by tests
if (!successThemerrData)
{
return;

Check warning on line 165 in Jellyfin.Plugin.Themerr/ThemerrManager.cs

View check run for this annotation

Codecov / codecov/patch

Jellyfin.Plugin.Themerr/ThemerrManager.cs#L164-L165

Added lines #L164 - L165 were not covered by tests
}

movie.RefreshMetadata(CancellationToken.None);
}

/// <summary>
/// Check if the theme song should be downloaded.
///
/// If theme.mp3 exists and themerr.json doesn't exist, then skip to avoid overwriting user supplied themes.
/// Various checks are performed to determine if the theme song should be downloaded.
/// </summary>
/// <param name="themePath">The path to the theme song.</param>
/// <param name="themerrDataPath">The path to the themerr data file.</param>
/// <returns>True if the theme song should NOT be downloaded, false otherwise.</returns>
public bool ShouldSkipDownload(string themePath, string themerrDataPath)
/// <returns>True to continue with downloaded, false otherwise.</returns>
public bool ContinueDownload(string themePath, string themerrDataPath)
{
return System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath);
if (!System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath))
{
// neither file exists, so don't skip
return true;
}

if (!System.IO.File.Exists(themePath) && System.IO.File.Exists(themerrDataPath))
{
// the theme is missing, so delete the themerr data file
System.IO.File.Delete(themerrDataPath);
return true;
}

if (System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath))
{
// the theme is user supplied, so don't overwrite it
return false;
}

var existingThemeMd5 = GetExistingThemerrDataValue("theme_md5", themerrDataPath);

// if existing theme md5 is empty, don't skip
if (string.IsNullOrEmpty(existingThemeMd5))
{
return true;
}

// check if the theme hash matches what is in the themerr data file
var themeMd5 = GetMd5Hash(themePath);

// if hashes match, theme is supplied by themerr, otherwise it is user supplied
return themeMd5 == existingThemeMd5;
}

/// <summary>
Expand Down Expand Up @@ -226,15 +268,17 @@
/// <summary>
/// Save the themerr data file.
/// </summary>
/// <param name="themePath">The path to the theme song.</param>
/// <param name="themerrDataPath">The path to the themerr data file.</param>
/// <param name="youtubeThemeUrl">The YouTube theme url.</param>
/// <returns>True if the file was saved successfully, false otherwise.</returns>
public bool SaveThemerrData(string themerrDataPath, string youtubeThemeUrl)
public bool SaveThemerrData(string themePath, string themerrDataPath, string youtubeThemeUrl)
{
var success = false;
var themerrData = new
{
downloaded_timestamp = DateTime.UtcNow,
theme_md5 = GetMd5Hash(themePath),
youtube_theme_url = youtubeThemeUrl
};
try
Expand All @@ -250,6 +294,23 @@
return success && WaitForFile(themerrDataPath, 10000);
}

/// <summary>
/// Get the MD5 hash of a file.
/// </summary>
/// <param name="filePath">The file path.</param>
/// <returns>The MD5 hash of the file.</returns>
public string GetMd5Hash(string filePath)
{
using (var md5 = System.Security.Cryptography.MD5.Create())
{
using (var stream = System.IO.File.OpenRead(filePath))
{
var hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}
}
}

/// <summary>
/// Wait for file to exist on disk and is not locked by another process.
/// </summary>
Expand Down