Bulk Operations (#596)

* Implemented the ability to perform multi-selections on cards. Basic selection code is done, CSS needed and exposing actions.

* Implemented a bulk selection bar. Added logic to the card on when to force show checkboxes.

* Fixed some bad parsing groups and cases for Comic Chapters.

* Hooked up some bulk actions on series detail page. Not hooked up to backend yet.

* Fixes #593. URI Enocde library names as sometimes they can have & in them.

* Implemented the ability to mark volume/chapters as read/unread.

* Hooked up mark as unread with specials as well.

* Add to reading list hooked up for Series Detail

* Implemented ability to add multiple series to a reading list.

* Implemented bulk selection for series cards

* Added comments to the new code in ReaderService.cs

* Implemented proper styling on bulk operation bar and integrated for collections.

* Fixed an issue with shift clicking

* Cleaned up css of bulk operations bar

* Code cleanup
This commit is contained in:
Joseph Milazzo 2021-09-24 17:27:47 -07:00 committed by GitHub
parent 52c4285168
commit f5229fd0e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1129 additions and 172 deletions

View file

@ -101,27 +101,7 @@ namespace API.Controllers
user.Progresses ??= new List<AppUserProgress>();
foreach (var volume in volumes)
{
foreach (var chapter in volume.Chapters)
{
var userProgress = GetUserProgressForChapter(user, chapter);
if (userProgress == null)
{
user.Progresses.Add(new AppUserProgress
{
PagesRead = chapter.Pages,
VolumeId = volume.Id,
SeriesId = markReadDto.SeriesId,
ChapterId = chapter.Id
});
}
else
{
userProgress.PagesRead = chapter.Pages;
userProgress.SeriesId = markReadDto.SeriesId;
userProgress.VolumeId = volume.Id;
}
}
_readerService.MarkChaptersAsRead(user, markReadDto.SeriesId, volume.Chapters);
}
_unitOfWork.UserRepository.Update(user);
@ -135,30 +115,6 @@ namespace API.Controllers
return BadRequest("There was an issue saving progress");
}
private static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
{
AppUserProgress userProgress = null;
try
{
userProgress =
user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
}
catch (Exception)
{
// There is a very rare chance that user progress will duplicate current row. If that happens delete one with less pages
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
if (progresses.Count > 1)
{
user.Progresses = new List<AppUserProgress>()
{
user.Progresses.First()
};
userProgress = user.Progresses.First();
}
}
return userProgress;
}
/// <summary>
/// Marks a Series as Unread (progress)
@ -175,7 +131,7 @@ namespace API.Controllers
{
foreach (var chapter in volume.Chapters)
{
var userProgress = GetUserProgressForChapter(user, chapter);
var userProgress = ReaderService.GetUserProgressForChapter(user, chapter);
if (userProgress == null) continue;
userProgress.PagesRead = 0;
@ -206,28 +162,7 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
foreach (var chapter in chapters)
{
user.Progresses ??= new List<AppUserProgress>();
var userProgress = user.Progresses.FirstOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
if (userProgress == null)
{
user.Progresses.Add(new AppUserProgress
{
PagesRead = 0,
VolumeId = markVolumeReadDto.VolumeId,
SeriesId = markVolumeReadDto.SeriesId,
ChapterId = chapter.Id
});
}
else
{
userProgress.PagesRead = 0;
userProgress.SeriesId = markVolumeReadDto.SeriesId;
userProgress.VolumeId = markVolumeReadDto.VolumeId;
}
}
_readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
_unitOfWork.UserRepository.Update(user);
@ -250,27 +185,119 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
foreach (var chapter in chapters)
{
user.Progresses ??= new List<AppUserProgress>();
var userProgress = user.Progresses.FirstOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
_readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
if (userProgress == null)
{
user.Progresses.Add(new AppUserProgress
{
PagesRead = chapter.Pages,
VolumeId = markVolumeReadDto.VolumeId,
SeriesId = markVolumeReadDto.SeriesId,
ChapterId = chapter.Id
});
}
else
{
userProgress.PagesRead = chapter.Pages;
userProgress.SeriesId = markVolumeReadDto.SeriesId;
userProgress.VolumeId = markVolumeReadDto.VolumeId;
}
_unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
return BadRequest("Could not save progress");
}
/// <summary>
/// Marks all chapters within a list of volumes as Read. All volumes must belong to the same Series.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("mark-multiple-read")]
public async Task<ActionResult> MarkMultipleAsRead(MarkVolumesReadDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
user.Progresses ??= new List<AppUserProgress>();
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
foreach (var chapterId in dto.ChapterIds)
{
chapterIds.Add(chapterId);
}
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
_readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters);
_unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
return BadRequest("Could not save progress");
}
/// <summary>
/// Marks all chapters within a list of volumes as Unread. All volumes must belong to the same Series.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("mark-multiple-unread")]
public async Task<ActionResult> MarkMultipleAsUnread(MarkVolumesReadDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
user.Progresses ??= new List<AppUserProgress>();
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
foreach (var chapterId in dto.ChapterIds)
{
chapterIds.Add(chapterId);
}
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
_readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters);
_unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
return BadRequest("Could not save progress");
}
/// <summary>
/// Marks all chapters within a list of series as Read.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("mark-multiple-series-read")]
public async Task<ActionResult> MarkMultipleSeriesAsRead(MarkMultipleSeriesAsReadDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
user.Progresses ??= new List<AppUserProgress>();
var volumes = await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
foreach (var volume in volumes)
{
_readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
}
_unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
return BadRequest("Could not save progress");
}
/// <summary>
/// Marks all chapters within a list of series as Unread.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("mark-multiple-series-unread")]
public async Task<ActionResult> MarkMultipleSeriesAsUnread(MarkMultipleSeriesAsReadDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
user.Progresses ??= new List<AppUserProgress>();
var volumes = await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
foreach (var volume in volumes)
{
_readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
}
_unitOfWork.UserRepository.Update(user);

View file

@ -212,7 +212,7 @@ namespace API.Controllers
}
/// <summary>
/// Update a
/// Update the properites (title, summary) of a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
@ -242,6 +242,11 @@ namespace API.Controllers
return BadRequest("Could not update reading list");
}
/// <summary>
/// Adds all chapters from a Series to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-series")]
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
{
@ -273,6 +278,86 @@ namespace API.Controllers
return Ok("Nothing to do");
}
/// <summary>
/// Adds all chapters from a list of volumes and chapters to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-multiple")]
public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
foreach (var chapterId in dto.ChapterIds)
{
chapterIds.Add(chapterId);
}
// If there are adds, tell tracking this has been modified
if (await AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
/// <summary>
/// Adds all chapters from a list of series to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-multiple-series")]
public async Task<ActionResult> UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
foreach (var seriesId in ids.Keys)
{
// If there are adds, tell tracking this has been modified
if (await AddChaptersToReadingList(seriesId, ids[seriesId], readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
[HttpPost("update-by-volume")]
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
{

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace API.DTOs.Reader
{
public class MarkMultipleSeriesAsReadDto
{
public IReadOnlyList<int> SeriesIds { get; init; }
}
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace API.DTOs.Reader
{
/// <summary>
/// This is used for bulk updating a set of volume and or chapters in one go
/// </summary>
public class MarkVolumesReadDto
{
public int SeriesId { get; set; }
/// <summary>
/// A list of Volumes to mark read
/// </summary>
public IReadOnlyList<int> VolumeIds { get; set; }
/// <summary>
/// A list of additional Chapters to mark as read
/// </summary>
public IReadOnlyList<int> ChapterIds { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace API.DTOs.ReadingLists
{
public class UpdateReadingListByMultipleDto
{
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
public IReadOnlyList<int> VolumeIds { get; init; }
public IReadOnlyList<int> ChapterIds { get; init; }
}
}

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace API.DTOs.ReadingLists
{
public class UpdateReadingListByMultipleSeriesDto
{
public int ReadingListId { get; init; }
public IReadOnlyList<int> SeriesIds { get; init; }
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -188,11 +189,16 @@ namespace API.Data.Repositories
/// </summary>
/// <param name="seriesIds"></param>
/// <returns></returns>
public async Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds)
public async Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false)
{
return await _context.Volume
.Where(v => seriesIds.Contains(v.SeriesId))
.ToListAsync();
var query = _context.Volume
.Where(v => seriesIds.Contains(v.SeriesId));
if (includeChapters)
{
query = query.Include(v => v.Chapters);
}
return await query.ToListAsync();
}
public async Task<bool> DeleteSeriesAsync(int seriesId)
@ -237,6 +243,35 @@ namespace API.Data.Repositories
return chapterIds.ToArray();
}
/// <summary>
/// This returns a list of tuples<chapterId, seriesId> back for each series id passed
/// </summary>
/// <param name="seriesIds"></param>
/// <returns></returns>
public async Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds)
{
var volumes = await _context.Volume
.Where(v => seriesIds.Contains(v.SeriesId))
.Include(v => v.Chapters)
.ToListAsync();
var seriesChapters = new Dictionary<int, IList<int>>();
foreach (var v in volumes)
{
foreach (var c in v.Chapters)
{
if (!seriesChapters.ContainsKey(v.SeriesId))
{
var list = new List<int>();
seriesChapters.Add(v.SeriesId, list);
}
seriesChapters[v.SeriesId].Add(c.Id);
}
}
return seriesChapters;
}
public async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
{
var userProgress = await _context.AppUserProgresses

View file

@ -44,5 +44,13 @@ namespace API.Data.Repositories
.AsNoTracking()
.SingleOrDefaultAsync();
}
public async Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds)
{
return await _context.Chapter
.Where(c => volumeIds.Contains(c.VolumeId))
.Select(c => c.Id)
.ToListAsync();
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using API.DTOs;
@ -44,11 +45,12 @@ namespace API.Interfaces.Repositories
/// <param name="volumeId"></param>
/// <returns></returns>
Task<VolumeDto> GetVolumeDtoAsync(int volumeId);
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds);
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
Task<bool> DeleteSeriesAsync(int seriesId);
Task<Volume> GetVolumeByIdAsync(int volumeId);
Task<Series> GetSeriesByIdAsync(int seriesId);
Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds);
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
/// <summary>
/// Used to add Progress/Rating information to series list.
/// </summary>

View file

@ -10,5 +10,6 @@ namespace API.Interfaces.Repositories
void Update(Volume volume);
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
Task<string> GetVolumeCoverImageAsync(int volumeId);
Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
}
}

View file

@ -1,10 +1,14 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
namespace API.Interfaces.Services
{
public interface IReaderService
{
void MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
void MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
Task<bool> SaveReadingProgress(ProgressDto progressDto, int userId);
Task<int> CapPageToChapter(int chapterId, int page);
Task<int> GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId);

View file

@ -25,6 +25,99 @@ namespace API.Interfaces.Services
_logger = logger;
}
/// <summary>
/// Marks all Chapters as Read by creating or updating UserProgress rows. Does not commit.
/// </summary>
/// <param name="user"></param>
/// <param name="seriesId"></param>
/// <param name="chapters"></param>
public void MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
{
foreach (var chapter in chapters)
{
var userProgress = GetUserProgressForChapter(user, chapter);
if (userProgress == null)
{
user.Progresses.Add(new AppUserProgress
{
PagesRead = chapter.Pages,
VolumeId = chapter.VolumeId,
SeriesId = seriesId,
ChapterId = chapter.Id
});
}
else
{
userProgress.PagesRead = chapter.Pages;
userProgress.SeriesId = seriesId;
userProgress.VolumeId = chapter.VolumeId;
}
}
}
/// <summary>
/// Marks all Chapters as Unread by creating or updating UserProgress rows. Does not commit.
/// </summary>
/// <param name="user"></param>
/// <param name="seriesId"></param>
/// <param name="chapters"></param>
public void MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
{
foreach (var chapter in chapters)
{
var userProgress = GetUserProgressForChapter(user, chapter);
if (userProgress == null)
{
user.Progresses.Add(new AppUserProgress
{
PagesRead = 0,
VolumeId = chapter.VolumeId,
SeriesId = seriesId,
ChapterId = chapter.Id
});
}
else
{
userProgress.PagesRead = 0;
userProgress.SeriesId = seriesId;
userProgress.VolumeId = chapter.VolumeId;
}
}
}
/// <summary>
/// Gets the User Progress for a given Chapter. This will handle any duplicates that might have occured in past versions and will delete them. Does not commit.
/// </summary>
/// <param name="user"></param>
/// <param name="chapter"></param>
/// <returns></returns>
public static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
{
AppUserProgress userProgress = null;
try
{
userProgress =
user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
}
catch (Exception)
{
// There is a very rare chance that user progress will duplicate current row. If that happens delete one with less pages
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
if (progresses.Count > 1)
{
user.Progresses = new List<AppUserProgress>()
{
user.Progresses.First()
};
userProgress = user.Progresses.First();
}
}
return userProgress;
}
/// <summary>
/// Saves progress to DB
/// </summary>
@ -82,6 +175,12 @@ namespace API.Interfaces.Services
return false;
}
/// <summary>
/// Ensures that the page is within 0 and total pages for a chapter. Makes one DB call.
/// </summary>
/// <param name="chapterId"></param>
/// <param name="page"></param>
/// <returns></returns>
public async Task<int> CapPageToChapter(int chapterId, int page)
{
var totalPages = await _unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId);

View file

@ -383,22 +383,22 @@ namespace API.Parser
RegexTimeout),
// Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex(
@"^(?<Series>.*)(?: |_)v(?<Volume>\d+)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
// Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex(
@"^(?<Series>.*)(?: (?<Volume>\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
// Batman & Robin the Teen Wonder #0
new Regex(
@"^(?<Series>.*)(?: |_)#(?<Volume>\d+)",
@"^(?<Series>.+?)(?: |_)v(?<Volume>\d+)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
// Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr
new Regex(
@"^(?<Series>.*)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-",
@"^(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
// Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex(
@"^(?<Series>.+?)(?: (?<Chapter>\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
// Batman & Robin the Teen Wonder #0
new Regex(
@"^(?<Series>.+?)(?:\s|_)#(?<Chapter>\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
// Saga 001 (2012) (Digital) (Empire-Zone)
@ -408,12 +408,12 @@ namespace API.Parser
RegexTimeout),
// Amazing Man Comics chapter 25
new Regex(
@"^(?!Vol)(?<Series>.*)( |_)c(hapter)( |_)(?<Chapter>\d*)",
@"^(?!Vol)(?<Series>.+?)( |_)c(hapter)( |_)(?<Chapter>\d*)",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
// Amazing Man Comics issue #25
new Regex(
@"^(?!Vol)(?<Series>.*)( |_)i(ssue)( |_) #(?<Chapter>\d*)",
@"^(?!Vol)(?<Series>.+?)( |_)i(ssue)( |_) #(?<Chapter>\d*)",
RegexOptions.IgnoreCase | RegexOptions.Compiled,
RegexTimeout),
};