mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-01 04:34:49 -04:00
Hashing (#634)
* Added the ability to see when a scan started at. * filename based hashing now uses last write time as well to ensure if the underlying file changes it sends a new copy * Fixed a bug where we would reset dark mode on the book reader to dark mode if our site was on dark mode, despite user setting light mode. * Added a single feed entry when some sort of collection, reading list, etc doesn't have anything in it. * Allow + in the normalization to prevent some series that use + to denote the sequel from not getting merged together.
This commit is contained in:
parent
0dbd45a41d
commit
c5b62d00d0
@ -142,6 +142,8 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Darker Than Black", "darkerthanblack")]
|
[InlineData("Darker Than Black", "darkerthanblack")]
|
||||||
[InlineData("Darker Than Black - Something", "darkerthanblacksomething")]
|
[InlineData("Darker Than Black - Something", "darkerthanblacksomething")]
|
||||||
[InlineData("Darker Than_Black", "darkerthanblack")]
|
[InlineData("Darker Than_Black", "darkerthanblack")]
|
||||||
|
[InlineData("Citrus", "citrus")]
|
||||||
|
[InlineData("Citrus+", "citrus+")]
|
||||||
[InlineData("", "")]
|
[InlineData("", "")]
|
||||||
public void NormalizeTest(string input, string expected)
|
public void NormalizeTest(string input, string expected)
|
||||||
{
|
{
|
||||||
|
@ -170,14 +170,14 @@ namespace API.Controllers
|
|||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdmin(user);
|
||||||
|
|
||||||
IEnumerable <CollectionTagDto> tags;
|
IList<CollectionTagDto> tags;
|
||||||
if (isAdmin)
|
if (isAdmin)
|
||||||
{
|
{
|
||||||
tags = await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
tags = (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync()).ToList();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
tags = (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -199,6 +199,14 @@ namespace API.Controllers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tags.Count == 0)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Title = "Nothing here",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return CreateXmlResult(SerializeXml(feed));
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,13 +306,13 @@ namespace API.Controllers
|
|||||||
|
|
||||||
var feed = CreateFeed(readingList.Title + " Reading List", $"{apiKey}/reading-list/{readingListId}", apiKey);
|
var feed = CreateFeed(readingList.Title + " Reading List", $"{apiKey}/reading-list/{readingListId}", apiKey);
|
||||||
|
|
||||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).ToList();
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
feed.Entries.Add(new FeedEntry()
|
feed.Entries.Add(new FeedEntry()
|
||||||
{
|
{
|
||||||
Id = item.ChapterId.ToString(),
|
Id = item.ChapterId.ToString(),
|
||||||
Title = "Chapter " + item.ChapterNumber,
|
Title = $"{item.SeriesName} Chapter {item.ChapterNumber}",
|
||||||
Links = new List<FeedLink>()
|
Links = new List<FeedLink>()
|
||||||
{
|
{
|
||||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{item.SeriesId}/volume/{item.VolumeId}/chapter/{item.ChapterId}"),
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{item.SeriesId}/volume/{item.VolumeId}/chapter/{item.ChapterId}"),
|
||||||
@ -313,6 +321,14 @@ namespace API.Controllers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Title = "Nothing here",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return CreateXmlResult(SerializeXml(feed));
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
@ -371,6 +387,14 @@ namespace API.Controllers
|
|||||||
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recentlyAdded.Count == 0)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Title = "Nothing here",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return CreateXmlResult(SerializeXml(feed));
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
}
|
}
|
||||||
@ -402,6 +426,14 @@ namespace API.Controllers
|
|||||||
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pagedList.Count == 0)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Title = "Nothing here",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return CreateXmlResult(SerializeXml(feed));
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
@ -41,8 +42,9 @@ namespace API.Extensions
|
|||||||
public static void AddCacheHeader(this HttpResponse response, string filename)
|
public static void AddCacheHeader(this HttpResponse response, string filename)
|
||||||
{
|
{
|
||||||
if (filename == null || filename.Length <= 0) return;
|
if (filename == null || filename.Length <= 0) return;
|
||||||
|
var hashContent = filename + File.GetLastWriteTimeUtc(filename);
|
||||||
using var sha1 = new System.Security.Cryptography.SHA256CryptoServiceProvider();
|
using var sha1 = new System.Security.Cryptography.SHA256CryptoServiceProvider();
|
||||||
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(filename)).Select(x => x.ToString("X2"))));
|
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ namespace API.Parser
|
|||||||
MatchOptions,
|
MatchOptions,
|
||||||
RegexTimeout);
|
RegexTimeout);
|
||||||
|
|
||||||
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9]",
|
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9\+]",
|
||||||
MatchOptions,
|
MatchOptions,
|
||||||
RegexTimeout);
|
RegexTimeout);
|
||||||
|
|
||||||
|
@ -139,6 +139,8 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
var madeUpdate = false;
|
var madeUpdate = false;
|
||||||
if (series == null) return false;
|
if (series == null) return false;
|
||||||
|
|
||||||
|
// NOTE: This will fail if we replace the cover of the first volume on a first scan. Because the series will already have a cover image
|
||||||
if (ShouldUpdateCoverImage(series.CoverImage, null, forceUpdate, series.CoverImageLocked))
|
if (ShouldUpdateCoverImage(series.CoverImage, null, forceUpdate, series.CoverImageLocked))
|
||||||
{
|
{
|
||||||
series.Volumes ??= new List<Volume>();
|
series.Volumes ??= new List<Volume>();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using System.Threading;
|
using System;
|
||||||
using API.DTOs.Update;
|
using API.DTOs.Update;
|
||||||
|
|
||||||
namespace API.SignalR
|
namespace API.SignalR
|
||||||
@ -46,19 +46,6 @@ namespace API.SignalR
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalRMessage ScanLibraryEvent(int libraryId, string stage)
|
|
||||||
{
|
|
||||||
return new SignalRMessage()
|
|
||||||
{
|
|
||||||
Name = SignalREvents.ScanLibrary,
|
|
||||||
Body = new
|
|
||||||
{
|
|
||||||
LibraryId = libraryId,
|
|
||||||
Stage = stage
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SignalRMessage ScanLibraryProgressEvent(int libraryId, float progress)
|
public static SignalRMessage ScanLibraryProgressEvent(int libraryId, float progress)
|
||||||
{
|
{
|
||||||
return new SignalRMessage()
|
return new SignalRMessage()
|
||||||
@ -68,6 +55,7 @@ namespace API.SignalR
|
|||||||
{
|
{
|
||||||
LibraryId = libraryId,
|
LibraryId = libraryId,
|
||||||
Progress = progress,
|
Progress = progress,
|
||||||
|
EventTime = DateTime.Now
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export interface ScanLibraryProgressEvent {
|
export interface ScanLibraryProgressEvent {
|
||||||
libraryId: number;
|
libraryId: number;
|
||||||
progress: number;
|
progress: number;
|
||||||
|
eventTime: string;
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<span id="library-name--{{idx}}">{{library.name}}</span>
|
<span id="library-name--{{idx}}">{{library.name}}</span>
|
||||||
<div class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" *ngIf="scanInProgress.hasOwnProperty(library.id) && scanInProgress[library.id]" title="Scan in progress">
|
<div class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" *ngIf="scanInProgress.hasOwnProperty(library.id) && scanInProgress[library.id].progress" title="Scan in progress. Started at {{scanInProgress[library.id].timestamp | date: 'short'}}">
|
||||||
<span class="sr-only">Scan for {{library.name}} in progress</span>
|
<span class="sr-only">Scan for {{library.name}} in progress</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
|
@ -24,7 +24,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
|||||||
* If a deletion is in progress for a library
|
* If a deletion is in progress for a library
|
||||||
*/
|
*/
|
||||||
deletionInProgress: boolean = false;
|
deletionInProgress: boolean = false;
|
||||||
scanInProgress: {[key: number]: boolean} = {};
|
scanInProgress: {[key: number]: {progress: boolean, timestamp?: string}} = {};
|
||||||
libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`;
|
libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`;
|
||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
@ -41,9 +41,12 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
|||||||
if (event.event != EVENTS.ScanLibraryProgress) return;
|
if (event.event != EVENTS.ScanLibraryProgress) return;
|
||||||
|
|
||||||
const scanEvent = event.payload as ScanLibraryProgressEvent;
|
const scanEvent = event.payload as ScanLibraryProgressEvent;
|
||||||
this.scanInProgress[scanEvent.libraryId] = scanEvent.progress !== 100;
|
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 100};
|
||||||
|
if (scanEvent.progress === 0) {
|
||||||
|
this.scanInProgress[scanEvent.libraryId].timestamp = scanEvent.eventTime;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.scanInProgress[scanEvent.libraryId] === false && scanEvent.progress === 100) {
|
if (this.scanInProgress[scanEvent.libraryId].progress === false && scanEvent.progress === 100) {
|
||||||
this.libraryService.getLibraries().pipe(take(1)).subscribe(libraries => {
|
this.libraryService.getLibraries().pipe(take(1)).subscribe(libraries => {
|
||||||
const newLibrary = libraries.find(lib => lib.id === scanEvent.libraryId);
|
const newLibrary = libraries.find(lib => lib.id === scanEvent.libraryId);
|
||||||
const existingLibrary = this.libraries.find(lib => lib.id === scanEvent.libraryId);
|
const existingLibrary = this.libraries.find(lib => lib.id === scanEvent.libraryId);
|
||||||
|
@ -558,9 +558,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
margin = this.user.preferences.bookReaderMargin + '%';
|
margin = this.user.preferences.bookReaderMargin + '%';
|
||||||
}
|
}
|
||||||
this.pageStyles = {'font-family': this.user.preferences.bookReaderFontFamily, 'font-size': this.user.preferences.bookReaderFontSize + '%', 'margin-left': margin, 'margin-right': margin, 'line-height': this.user.preferences.bookReaderLineSpacing + '%'};
|
this.pageStyles = {'font-family': this.user.preferences.bookReaderFontFamily, 'font-size': this.user.preferences.bookReaderFontSize + '%', 'margin-left': margin, 'margin-right': margin, 'line-height': this.user.preferences.bookReaderLineSpacing + '%'};
|
||||||
if (this.user.preferences.siteDarkMode && !this.user.preferences.bookReaderDarkMode) {
|
if (!afterSave) {
|
||||||
this.user.preferences.bookReaderDarkMode = true;
|
if (this.user.preferences.siteDarkMode && !this.user.preferences.bookReaderDarkMode) {
|
||||||
|
this.user.preferences.bookReaderDarkMode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleDarkMode(this.user.preferences.bookReaderDarkMode);
|
this.toggleDarkMode(this.user.preferences.bookReaderDarkMode);
|
||||||
} else {
|
} else {
|
||||||
this.pageStyles = {'font-family': 'default', 'font-size': '100%', 'margin-left': margin, 'margin-right': margin, 'line-height': '100%'};
|
this.pageStyles = {'font-family': 'default', 'font-size': '100%', 'margin-left': margin, 'margin-right': margin, 'line-height': '100%'};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user