mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-04-04 07:12:27 -04:00
Code Cleanup
Make fields readonly Remove unnecessary casts Format document Remove unnecessary usings Sort usings Use file-level namespaces Order modifiers
This commit is contained in:
@@ -41,10 +41,10 @@ public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
|
||||
}
|
||||
|
||||
|
||||
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
{
|
||||
if (AaxFile is null) return false;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await (AaxConversion = decryptMultiAsync(AaxFile, DownloadOptions.ChapterInfo));
|
||||
|
||||
@@ -45,7 +45,7 @@ public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
|
||||
}
|
||||
}
|
||||
|
||||
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
{
|
||||
if (AaxFile is null) return false;
|
||||
outputTempFile = GetNewTempFilePath(DownloadOptions.OutputFormat.ToString());
|
||||
@@ -56,7 +56,7 @@ public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
|
||||
|
||||
try
|
||||
{
|
||||
await (AaxConversion = decryptAsync(AaxFile, outputFile));
|
||||
await (AaxConversion = decryptAsync(AaxFile, outputFile));
|
||||
|
||||
return AaxConversion.IsCompletedSuccessfully;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public abstract class AudiobookDownloadBase
|
||||
private readonly string tempFilePath;
|
||||
|
||||
protected AudiobookDownloadBase(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
{
|
||||
{
|
||||
OutputDirectory = ArgumentValidator.EnsureNotNullOrWhiteSpace(outDirectory, nameof(outDirectory));
|
||||
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
|
||||
DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed;
|
||||
|
||||
@@ -66,13 +66,13 @@ public static class LinqStats
|
||||
/// 2-tailed t-Distribution critical values at 75%, 80%, 85%,
|
||||
/// 90%, 95%, and 99% confidence for 1 - 201 degrees of freedom.
|
||||
/// </summary>
|
||||
private readonly static double[][] T_TABLE;
|
||||
private readonly static double[] T_Table_25 = { 2.414213562, 1.603567451, 1.422625281, 1.344397556, 1.300949037, 1.273349309, 1.254278682, 1.240318261, 1.229659173, 1.221255395, 1.214460246, 1.208852542, 1.204146242, 1.200140298, 1.196689284, 1.193685414, 1.191047107, 1.188711483, 1.186629298, 1.184761434, 1.183076432, 1.181548697, 1.180157199, 1.178884497, 1.177716003, 1.176639425, 1.175644329, 1.174721803, 1.173864189, 1.173064871, 1.1723181, 1.17161886, 1.170962753, 1.17034591, 1.169764906, 1.169216709, 1.168698615, 1.168208212, 1.167743338, 1.167302049, 1.166882595, 1.166483396, 1.166103019, 1.165740162, 1.165393644, 1.165062385, 1.164745398, 1.164441782, 1.164150707, 1.163871412, 1.163603196, 1.163345413, 1.163097467, 1.162858803, 1.162628911, 1.162407316, 1.162193577, 1.161987283, 1.161788052, 1.161595527, 1.161409375, 1.161229286, 1.161054967, 1.160886145, 1.160722566, 1.160563987, 1.160410184, 1.160260944, 1.160116066, 1.159975363, 1.159838656, 1.159705777, 1.159576569, 1.15945088, 1.15932857, 1.159209503, 1.159093552, 1.158980598, 1.158870524, 1.158763222, 1.158658589, 1.158556526, 1.15845694, 1.158359742, 1.158264847, 1.158172173, 1.158081645, 1.157993188, 1.157906731, 1.157822209, 1.157739556, 1.157658712, 1.157579617, 1.157502216, 1.157426454, 1.157352281, 1.157279646, 1.157208502, 1.157138804, 1.157070509, 1.157003573, 1.156937958, 1.156873624, 1.156810534, 1.156748653, 1.156687945, 1.156628379, 1.156569922, 1.156512543, 1.156456213, 1.156400904, 1.156346587, 1.156293237, 1.156240827, 1.156189334, 1.156138733, 1.156089001, 1.156040117, 1.155992058, 1.155944804, 1.155898335, 1.155852631, 1.155807674, 1.155763446, 1.155719928, 1.155677105, 1.155634959, 1.155593475, 1.155552637, 1.15551243, 1.155472839, 1.155433851, 1.155395452, 1.155357629, 1.155320368, 1.155283658, 1.155247486, 1.155211841, 1.15517671, 1.155142084, 1.15510795, 1.1550743, 1.155041122, 1.155008406, 1.154976144, 1.154944326, 1.154912942, 1.154881984, 1.154851443, 1.154821311, 1.15479158, 1.154762241, 1.154733287, 1.154704711, 1.154676505, 1.154648662, 1.154621175, 1.154594037, 1.154567242, 1.154540783, 1.154514654, 1.154488849, 1.154463361, 1.154438185, 1.154413316, 1.154388747, 1.154364474, 1.15434049, 1.154316792, 1.154293373, 1.154270229, 1.154247355, 1.154224746, 1.154202398, 1.154180307, 1.154158467, 1.154136875, 1.154115526, 1.154094417, 1.154073543, 1.1540529, 1.154032485, 1.154012294, 1.153992323, 1.153972568, 1.153953027, 1.153933695, 1.15391457, 1.153895647, 1.153876925, 1.153858399, 1.153840066, 1.153821925, 1.15380397, 1.153786201, 1.153768613, 1.153751204, 1.153733972, 1.153716914, 1.153700026 };
|
||||
private readonly static double[] T_Table_20 = { 3.077683537, 1.885618083, 1.637744354, 1.533206274, 1.475884049, 1.439755747, 1.414923928, 1.39681531, 1.383028738, 1.372183641, 1.363430318, 1.356217334, 1.350171289, 1.345030374, 1.340605608, 1.336757167, 1.33337939, 1.330390944, 1.327728209, 1.325340707, 1.323187874, 1.321236742, 1.31946024, 1.317835934, 1.316345073, 1.314971864, 1.313702913, 1.312526782, 1.311433647, 1.310415025, 1.309463549, 1.308572793, 1.307737124, 1.306951587, 1.306211802, 1.305513886, 1.304854381, 1.304230204, 1.303638589, 1.303077053, 1.302543359, 1.302035487, 1.301551608, 1.30109006, 1.300649332, 1.300228048, 1.299824947, 1.299438879, 1.299068785, 1.298713694, 1.298372713, 1.298045016, 1.297729843, 1.297426488, 1.2971343, 1.296852673, 1.296581044, 1.29631889, 1.296065725, 1.295821094, 1.295584571, 1.295355762, 1.295134294, 1.29491982, 1.294712013, 1.294510568, 1.294315197, 1.294125629, 1.293941609, 1.293762898, 1.293589269, 1.293420507, 1.293256413, 1.293096793, 1.292941469, 1.292790268, 1.292643029, 1.292499597, 1.292359828, 1.292223583, 1.29209073, 1.291961144, 1.291834705, 1.291711301, 1.291590824, 1.291473171, 1.291358243, 1.291245948, 1.291136195, 1.291028899, 1.290923979, 1.290821356, 1.290720956, 1.290622708, 1.290526543, 1.290432395, 1.290340202, 1.290249904, 1.290161442, 1.290074761, 1.289989809, 1.289906533, 1.289824884, 1.289744816, 1.289666283, 1.289589241, 1.289513648, 1.289439464, 1.289366649, 1.289295166, 1.289224979, 1.289156054, 1.289088355, 1.289021851, 1.28895651, 1.288892302, 1.288829199, 1.288767171, 1.288706191, 1.288646234, 1.288587273, 1.288529284, 1.288472243, 1.288416127, 1.288360913, 1.288306581, 1.288253109, 1.288200477, 1.288148665, 1.288097654, 1.288047427, 1.287997964, 1.287949248, 1.287901264, 1.287853994, 1.287807422, 1.287761534, 1.287716314, 1.287671748, 1.287627821, 1.287584521, 1.287541833, 1.287499745, 1.287458245, 1.287417319, 1.287376957, 1.287337146, 1.287297876, 1.287259135, 1.287220914, 1.2871832, 1.287145985, 1.287109259, 1.287073012, 1.287037235, 1.287001918, 1.286967053, 1.286932631, 1.286898644, 1.286865084, 1.286831942, 1.286799212, 1.286766884, 1.286734952, 1.286703409, 1.286672248, 1.286641461, 1.286611042, 1.286580985, 1.286551283, 1.286521929, 1.286492918, 1.286464244, 1.286435901, 1.286407882, 1.286380184, 1.286352799, 1.286325724, 1.286298952, 1.286272479, 1.286246299, 1.286220408, 1.286194801, 1.286169474, 1.286144421, 1.286119638, 1.286095122, 1.286070867, 1.28604687, 1.286023127, 1.285999633, 1.285976384, 1.285953377, 1.285930609, 1.285908074, 1.285885771, 1.285863694, 1.285841842, 1.285820209, 1.285798794 };
|
||||
private readonly static double[] T_Table_15 = { 4.16529977, 2.281930588, 1.924319657, 1.778192164, 1.699362566, 1.650173154, 1.616591737, 1.59222144, 1.573735785, 1.559235933, 1.547559766, 1.537956495, 1.529919606, 1.523095061, 1.517227969, 1.51213017, 1.507659754, 1.503707672, 1.500188756, 1.497035518, 1.494193795, 1.491619612, 1.489276897, 1.487135783, 1.485171326, 1.483362535, 1.481691617, 1.48014339, 1.478704821, 1.477364662, 1.47611315, 1.474941772, 1.473843072, 1.47281049, 1.471838233, 1.470921166, 1.470054719, 1.469234815, 1.468457801, 1.467720399, 1.467019655, 1.466352901, 1.465717725, 1.465111933, 1.464533534, 1.463980712, 1.463451805, 1.462945295, 1.46245979, 1.461994009, 1.461546775, 1.461117, 1.460703683, 1.460305896, 1.45992278, 1.459553538, 1.45919743, 1.458853767, 1.458521908, 1.458201256, 1.457891251, 1.457591373, 1.457301133, 1.457020074, 1.456747768, 1.45648381, 1.456227824, 1.455979454, 1.455738365, 1.455504241, 1.455276784, 1.455055715, 1.454840767, 1.45463169, 1.454428246, 1.454230212, 1.454037373, 1.453849529, 1.453666487, 1.453488066, 1.453314093, 1.453144404, 1.452978842, 1.452817259, 1.452659513, 1.452505469, 1.452354998, 1.452207977, 1.452064289, 1.451923821, 1.451786468, 1.451652126, 1.451520697, 1.451392088, 1.451266209, 1.451142973, 1.451022299, 1.450904108, 1.450788323, 1.450674871, 1.450563684, 1.450454694, 1.450347836, 1.450243048, 1.450140271, 1.450039448, 1.449940523, 1.449843444, 1.449748158, 1.449654617, 1.449562773, 1.449472581, 1.449383997, 1.449296977, 1.449211481, 1.449127468, 1.449044902, 1.448963744, 1.448883959, 1.448805513, 1.448728372, 1.448652503, 1.448577876, 1.44850446, 1.448432226, 1.448361146, 1.448291192, 1.448222337, 1.448154557, 1.448087826, 1.44802212, 1.447957415, 1.447893688, 1.447830919, 1.447769085, 1.447708165, 1.44764814, 1.44758899, 1.447530695, 1.447473238, 1.447416601, 1.447360765, 1.447305715, 1.447251433, 1.447197905, 1.447145113, 1.447093044, 1.447041682, 1.446991013, 1.446941023, 1.446891698, 1.446843026, 1.446794994, 1.446747588, 1.446700797, 1.446654609, 1.446609012, 1.446563996, 1.446519548, 1.446475659, 1.446432318, 1.446389514, 1.446347238, 1.44630548, 1.44626423, 1.44622348, 1.44618322, 1.446143442, 1.446104137, 1.446065296, 1.446026911, 1.445988975, 1.44595148, 1.445914417, 1.44587778, 1.445841561, 1.445805753, 1.445770349, 1.445735343, 1.445700727, 1.445666495, 1.445632641, 1.445599159, 1.445566042, 1.445533284, 1.445500881, 1.445468825, 1.445437112, 1.445405736, 1.445374691, 1.445343973, 1.445313576, 1.445283495, 1.445253726, 1.445224264, 1.445195103, 1.445166239, 1.445137668, 1.445109385, 1.445081387 };
|
||||
private readonly static double[] T_Table_10 = { 6.313751515, 2.91998558, 2.353363435, 2.131846786, 2.015048373, 1.943180281, 1.894578605, 1.859548038, 1.833112933, 1.812461123, 1.795884819, 1.782287556, 1.770933396, 1.761310136, 1.753050356, 1.745883676, 1.739606726, 1.734063607, 1.729132812, 1.724718243, 1.720742903, 1.717144374, 1.713871528, 1.71088208, 1.708140761, 1.70561792, 1.703288446, 1.701130934, 1.699127027, 1.697260887, 1.695518783, 1.693888748, 1.692360309, 1.690924255, 1.689572458, 1.688297714, 1.68709362, 1.68595446, 1.684875122, 1.683851013, 1.682878002, 1.681952357, 1.681070703, 1.680229977, 1.679427393, 1.678660414, 1.677926722, 1.677224196, 1.676550893, 1.675905025, 1.67528495, 1.674689154, 1.674116237, 1.673564906, 1.673033965, 1.672522303, 1.672028888, 1.671552762, 1.671093032, 1.670648865, 1.670219484, 1.669804163, 1.669402222, 1.669013025, 1.668635976, 1.668270514, 1.667916114, 1.667572281, 1.667238549, 1.666914479, 1.666599658, 1.666293696, 1.665996224, 1.665706893, 1.665425373, 1.665151353, 1.664884537, 1.664624645, 1.664371409, 1.664124579, 1.663883913, 1.663649184, 1.663420175, 1.663196679, 1.6629785, 1.662765449, 1.662557349, 1.662354029, 1.662155326, 1.661961084, 1.661771155, 1.661585397, 1.661403674, 1.661225855, 1.661051817, 1.66088144, 1.66071461, 1.660551217, 1.660391156, 1.660234326, 1.66008063, 1.659929976, 1.659782273, 1.659637437, 1.659495383, 1.659356034, 1.659219312, 1.659085144, 1.658953458, 1.658824187, 1.658697265, 1.658572629, 1.658450216, 1.658329969, 1.65821183, 1.658095744, 1.657981659, 1.657869522, 1.657759285, 1.657650899, 1.657544319, 1.657439499, 1.657336397, 1.65723497, 1.657135178, 1.657036982, 1.656940344, 1.656845226, 1.656751594, 1.656659413, 1.656568649, 1.65647927, 1.656391244, 1.656304542, 1.656219133, 1.656134988, 1.65605208, 1.655970382, 1.655889868, 1.655810511, 1.655732287, 1.655655173, 1.655579143, 1.655504177, 1.655430251, 1.655357345, 1.655285437, 1.655214506, 1.655144534, 1.6550755, 1.655007387, 1.654940175, 1.654873847, 1.654808385, 1.654743774, 1.654679996, 1.654617035, 1.654554875, 1.654493503, 1.654432901, 1.654373057, 1.654313957, 1.654255585, 1.654197929, 1.654140976, 1.654084713, 1.654029128, 1.653974208, 1.653919942, 1.653866317, 1.653813324, 1.653760949, 1.653709184, 1.653658017, 1.653607437, 1.653557435, 1.653508002, 1.653459126, 1.6534108, 1.653363013, 1.653315758, 1.653269024, 1.653222803, 1.653177088, 1.653131869, 1.653087138, 1.653042889, 1.652999113, 1.652955802, 1.652912949, 1.652870547, 1.652828589, 1.652787068, 1.652745977, 1.65270531, 1.652665059, 1.652625219, 1.652585784, 1.652546746, 1.652508101 };
|
||||
private readonly static double[] T_Table_05 = { 12.70620474, 4.30265273, 3.182446305, 2.776445105, 2.570581836, 2.446911851, 2.364624252, 2.306004135, 2.262157163, 2.228138852, 2.20098516, 2.17881283, 2.160368656, 2.144786688, 2.131449546, 2.119905299, 2.109815578, 2.10092204, 2.093024054, 2.085963447, 2.079613845, 2.073873068, 2.06865761, 2.063898562, 2.059538553, 2.055529439, 2.051830516, 2.048407142, 2.045229642, 2.042272456, 2.039513446, 2.036933343, 2.034515297, 2.032244509, 2.030107928, 2.028094001, 2.026192463, 2.024394164, 2.02269092, 2.02107539, 2.01954097, 2.018081703, 2.016692199, 2.015367574, 2.014103389, 2.012895599, 2.011740514, 2.010634758, 2.009575237, 2.008559112, 2.00758377, 2.006646805, 2.005745995, 2.004879288, 2.004044783, 2.003240719, 2.002465459, 2.001717484, 2.000995378, 2.000297822, 1.999623585, 1.998971517, 1.998340543, 1.997729654, 1.997137908, 1.996564419, 1.996008354, 1.995468931, 1.994945415, 1.994437112, 1.993943368, 1.993463567, 1.992997126, 1.992543495, 1.992102154, 1.99167261, 1.991254395, 1.990847069, 1.99045021, 1.990063421, 1.989686323, 1.989318557, 1.98895978, 1.988609667, 1.988267907, 1.987934206, 1.987608282, 1.987289865, 1.9869787, 1.986674541, 1.986377154, 1.986086317, 1.985801814, 1.985523442, 1.985251004, 1.984984312, 1.984723186, 1.984467455, 1.984216952, 1.983971519, 1.983731003, 1.983495259, 1.983264145, 1.983037526, 1.982815274, 1.982597262, 1.98238337, 1.982173483, 1.98196749, 1.981765282, 1.981566757, 1.981371815, 1.981180359, 1.980992298, 1.980807541, 1.980626002, 1.980447599, 1.980272249, 1.980099876, 1.979930405, 1.979763763, 1.979599878, 1.979438685, 1.979280117, 1.979124109, 1.978970602, 1.978819535, 1.97867085, 1.978524491, 1.978380405, 1.978238539, 1.978098842, 1.977961264, 1.977825758, 1.977692277, 1.977560777, 1.977431212, 1.977303542, 1.977177724, 1.97705372, 1.976931489, 1.976810994, 1.976692198, 1.976575066, 1.976459563, 1.976345655, 1.976233309, 1.976122494, 1.976013178, 1.975905331, 1.975798924, 1.975693928, 1.975590315, 1.975488058, 1.975387131, 1.975287508, 1.975189163, 1.975092073, 1.974996213, 1.97490156, 1.974808092, 1.974715786, 1.974624621, 1.974534576, 1.97444563, 1.974357764, 1.974270957, 1.974185191, 1.974100447, 1.974016708, 1.973933954, 1.973852169, 1.973771337, 1.97369144, 1.973612462, 1.973534388, 1.973457202, 1.973380889, 1.973305434, 1.973230823, 1.973157042, 1.973084077, 1.973011915, 1.972940542, 1.972869946, 1.972800114, 1.972731033, 1.972662692, 1.972595079, 1.972528182, 1.97246199, 1.972396491, 1.972331676, 1.972267533, 1.972204051, 1.972141222, 1.972079034, 1.972017478, 1.971956544, 1.971896224 };
|
||||
private readonly static double[] T_Table_01 = { 63.65674116, 9.924843201, 5.84090931, 4.604094871, 4.032142984, 3.707428021, 3.499483297, 3.355387331, 3.249835542, 3.169272673, 3.105806516, 3.054539589, 3.012275839, 2.976842734, 2.946712883, 2.920781622, 2.89823052, 2.878440473, 2.860934606, 2.84533971, 2.831359558, 2.818756061, 2.807335684, 2.796939505, 2.787435814, 2.778714533, 2.770682957, 2.763262455, 2.756385904, 2.749995654, 2.744041919, 2.738481482, 2.733276642, 2.728394367, 2.723805589, 2.71948463, 2.715408722, 2.711557602, 2.707913184, 2.704459267, 2.701181304, 2.698066186, 2.695102079, 2.692278266, 2.689585019, 2.687013492, 2.684555618, 2.682204027, 2.679951974, 2.677793271, 2.675722234, 2.673733631, 2.671822636, 2.669984796, 2.668215988, 2.666512398, 2.664870482, 2.663286954, 2.661758752, 2.660283029, 2.658857127, 2.657478565, 2.656145025, 2.654854337, 2.653604469, 2.652393515, 2.651219685, 2.650081299, 2.648976774, 2.647904624, 2.646863444, 2.645851913, 2.644868782, 2.643912872, 2.642983067, 2.642078313, 2.641197611, 2.640340015, 2.639504627, 2.638690596, 2.637897113, 2.63712341, 2.636368757, 2.635632458, 2.634913852, 2.634212309, 2.633527229, 2.632858038, 2.632204191, 2.631565166, 2.630940463, 2.630329608, 2.629732145, 2.629147638, 2.628575671, 2.628015844, 2.627467774, 2.626931096, 2.626405457, 2.625890521, 2.625385965, 2.624891476, 2.624406758, 2.623931523, 2.623465496, 2.623008411, 2.622560015, 2.622120061, 2.621688313, 2.621264543, 2.620848534, 2.620440073, 2.620038957, 2.619644989, 2.619257981, 2.618877749, 2.618504116, 2.618136914, 2.617775976, 2.617421145, 2.617072266, 2.616729191, 2.616391776, 2.616059883, 2.615733377, 2.615412127, 2.615096008, 2.614784899, 2.61447868, 2.614177238, 2.613880461, 2.613588242, 2.613300477, 2.613017065, 2.612737908, 2.61246291, 2.61219198, 2.611925028, 2.611661966, 2.611402711, 2.611147181, 2.610895295, 2.610646976, 2.61040215, 2.610160742, 2.609922682, 2.609687901, 2.609456331, 2.609227907, 2.609002566, 2.608780245, 2.608560883, 2.608344423, 2.608130807, 2.60791998, 2.607711886, 2.607506474, 2.607303692, 2.607103489, 2.606905817, 2.606710628, 2.606517876, 2.606327515, 2.606139501, 2.605953791, 2.605770342, 2.605589114, 2.605410067, 2.605233162, 2.605058359, 2.604885623, 2.604714916, 2.604546204, 2.60437945, 2.604214622, 2.604051686, 2.60389061, 2.603731363, 2.603573912, 2.603418229, 2.603264282, 2.603112045, 2.602961487, 2.602812582, 2.602665303, 2.602519622, 2.602375515, 2.602232955, 2.602091918, 2.60195238, 2.601814317, 2.601677705, 2.601542523, 2.601408747, 2.601276355, 2.601145327, 2.601015642, 2.600887278, 2.600760216, 2.600634436 };
|
||||
private static readonly double[][] T_TABLE;
|
||||
private static readonly double[] T_Table_25 = { 2.414213562, 1.603567451, 1.422625281, 1.344397556, 1.300949037, 1.273349309, 1.254278682, 1.240318261, 1.229659173, 1.221255395, 1.214460246, 1.208852542, 1.204146242, 1.200140298, 1.196689284, 1.193685414, 1.191047107, 1.188711483, 1.186629298, 1.184761434, 1.183076432, 1.181548697, 1.180157199, 1.178884497, 1.177716003, 1.176639425, 1.175644329, 1.174721803, 1.173864189, 1.173064871, 1.1723181, 1.17161886, 1.170962753, 1.17034591, 1.169764906, 1.169216709, 1.168698615, 1.168208212, 1.167743338, 1.167302049, 1.166882595, 1.166483396, 1.166103019, 1.165740162, 1.165393644, 1.165062385, 1.164745398, 1.164441782, 1.164150707, 1.163871412, 1.163603196, 1.163345413, 1.163097467, 1.162858803, 1.162628911, 1.162407316, 1.162193577, 1.161987283, 1.161788052, 1.161595527, 1.161409375, 1.161229286, 1.161054967, 1.160886145, 1.160722566, 1.160563987, 1.160410184, 1.160260944, 1.160116066, 1.159975363, 1.159838656, 1.159705777, 1.159576569, 1.15945088, 1.15932857, 1.159209503, 1.159093552, 1.158980598, 1.158870524, 1.158763222, 1.158658589, 1.158556526, 1.15845694, 1.158359742, 1.158264847, 1.158172173, 1.158081645, 1.157993188, 1.157906731, 1.157822209, 1.157739556, 1.157658712, 1.157579617, 1.157502216, 1.157426454, 1.157352281, 1.157279646, 1.157208502, 1.157138804, 1.157070509, 1.157003573, 1.156937958, 1.156873624, 1.156810534, 1.156748653, 1.156687945, 1.156628379, 1.156569922, 1.156512543, 1.156456213, 1.156400904, 1.156346587, 1.156293237, 1.156240827, 1.156189334, 1.156138733, 1.156089001, 1.156040117, 1.155992058, 1.155944804, 1.155898335, 1.155852631, 1.155807674, 1.155763446, 1.155719928, 1.155677105, 1.155634959, 1.155593475, 1.155552637, 1.15551243, 1.155472839, 1.155433851, 1.155395452, 1.155357629, 1.155320368, 1.155283658, 1.155247486, 1.155211841, 1.15517671, 1.155142084, 1.15510795, 1.1550743, 1.155041122, 1.155008406, 1.154976144, 1.154944326, 1.154912942, 1.154881984, 1.154851443, 1.154821311, 1.15479158, 1.154762241, 1.154733287, 1.154704711, 1.154676505, 1.154648662, 1.154621175, 1.154594037, 1.154567242, 1.154540783, 1.154514654, 1.154488849, 1.154463361, 1.154438185, 1.154413316, 1.154388747, 1.154364474, 1.15434049, 1.154316792, 1.154293373, 1.154270229, 1.154247355, 1.154224746, 1.154202398, 1.154180307, 1.154158467, 1.154136875, 1.154115526, 1.154094417, 1.154073543, 1.1540529, 1.154032485, 1.154012294, 1.153992323, 1.153972568, 1.153953027, 1.153933695, 1.15391457, 1.153895647, 1.153876925, 1.153858399, 1.153840066, 1.153821925, 1.15380397, 1.153786201, 1.153768613, 1.153751204, 1.153733972, 1.153716914, 1.153700026 };
|
||||
private static readonly double[] T_Table_20 = { 3.077683537, 1.885618083, 1.637744354, 1.533206274, 1.475884049, 1.439755747, 1.414923928, 1.39681531, 1.383028738, 1.372183641, 1.363430318, 1.356217334, 1.350171289, 1.345030374, 1.340605608, 1.336757167, 1.33337939, 1.330390944, 1.327728209, 1.325340707, 1.323187874, 1.321236742, 1.31946024, 1.317835934, 1.316345073, 1.314971864, 1.313702913, 1.312526782, 1.311433647, 1.310415025, 1.309463549, 1.308572793, 1.307737124, 1.306951587, 1.306211802, 1.305513886, 1.304854381, 1.304230204, 1.303638589, 1.303077053, 1.302543359, 1.302035487, 1.301551608, 1.30109006, 1.300649332, 1.300228048, 1.299824947, 1.299438879, 1.299068785, 1.298713694, 1.298372713, 1.298045016, 1.297729843, 1.297426488, 1.2971343, 1.296852673, 1.296581044, 1.29631889, 1.296065725, 1.295821094, 1.295584571, 1.295355762, 1.295134294, 1.29491982, 1.294712013, 1.294510568, 1.294315197, 1.294125629, 1.293941609, 1.293762898, 1.293589269, 1.293420507, 1.293256413, 1.293096793, 1.292941469, 1.292790268, 1.292643029, 1.292499597, 1.292359828, 1.292223583, 1.29209073, 1.291961144, 1.291834705, 1.291711301, 1.291590824, 1.291473171, 1.291358243, 1.291245948, 1.291136195, 1.291028899, 1.290923979, 1.290821356, 1.290720956, 1.290622708, 1.290526543, 1.290432395, 1.290340202, 1.290249904, 1.290161442, 1.290074761, 1.289989809, 1.289906533, 1.289824884, 1.289744816, 1.289666283, 1.289589241, 1.289513648, 1.289439464, 1.289366649, 1.289295166, 1.289224979, 1.289156054, 1.289088355, 1.289021851, 1.28895651, 1.288892302, 1.288829199, 1.288767171, 1.288706191, 1.288646234, 1.288587273, 1.288529284, 1.288472243, 1.288416127, 1.288360913, 1.288306581, 1.288253109, 1.288200477, 1.288148665, 1.288097654, 1.288047427, 1.287997964, 1.287949248, 1.287901264, 1.287853994, 1.287807422, 1.287761534, 1.287716314, 1.287671748, 1.287627821, 1.287584521, 1.287541833, 1.287499745, 1.287458245, 1.287417319, 1.287376957, 1.287337146, 1.287297876, 1.287259135, 1.287220914, 1.2871832, 1.287145985, 1.287109259, 1.287073012, 1.287037235, 1.287001918, 1.286967053, 1.286932631, 1.286898644, 1.286865084, 1.286831942, 1.286799212, 1.286766884, 1.286734952, 1.286703409, 1.286672248, 1.286641461, 1.286611042, 1.286580985, 1.286551283, 1.286521929, 1.286492918, 1.286464244, 1.286435901, 1.286407882, 1.286380184, 1.286352799, 1.286325724, 1.286298952, 1.286272479, 1.286246299, 1.286220408, 1.286194801, 1.286169474, 1.286144421, 1.286119638, 1.286095122, 1.286070867, 1.28604687, 1.286023127, 1.285999633, 1.285976384, 1.285953377, 1.285930609, 1.285908074, 1.285885771, 1.285863694, 1.285841842, 1.285820209, 1.285798794 };
|
||||
private static readonly double[] T_Table_15 = { 4.16529977, 2.281930588, 1.924319657, 1.778192164, 1.699362566, 1.650173154, 1.616591737, 1.59222144, 1.573735785, 1.559235933, 1.547559766, 1.537956495, 1.529919606, 1.523095061, 1.517227969, 1.51213017, 1.507659754, 1.503707672, 1.500188756, 1.497035518, 1.494193795, 1.491619612, 1.489276897, 1.487135783, 1.485171326, 1.483362535, 1.481691617, 1.48014339, 1.478704821, 1.477364662, 1.47611315, 1.474941772, 1.473843072, 1.47281049, 1.471838233, 1.470921166, 1.470054719, 1.469234815, 1.468457801, 1.467720399, 1.467019655, 1.466352901, 1.465717725, 1.465111933, 1.464533534, 1.463980712, 1.463451805, 1.462945295, 1.46245979, 1.461994009, 1.461546775, 1.461117, 1.460703683, 1.460305896, 1.45992278, 1.459553538, 1.45919743, 1.458853767, 1.458521908, 1.458201256, 1.457891251, 1.457591373, 1.457301133, 1.457020074, 1.456747768, 1.45648381, 1.456227824, 1.455979454, 1.455738365, 1.455504241, 1.455276784, 1.455055715, 1.454840767, 1.45463169, 1.454428246, 1.454230212, 1.454037373, 1.453849529, 1.453666487, 1.453488066, 1.453314093, 1.453144404, 1.452978842, 1.452817259, 1.452659513, 1.452505469, 1.452354998, 1.452207977, 1.452064289, 1.451923821, 1.451786468, 1.451652126, 1.451520697, 1.451392088, 1.451266209, 1.451142973, 1.451022299, 1.450904108, 1.450788323, 1.450674871, 1.450563684, 1.450454694, 1.450347836, 1.450243048, 1.450140271, 1.450039448, 1.449940523, 1.449843444, 1.449748158, 1.449654617, 1.449562773, 1.449472581, 1.449383997, 1.449296977, 1.449211481, 1.449127468, 1.449044902, 1.448963744, 1.448883959, 1.448805513, 1.448728372, 1.448652503, 1.448577876, 1.44850446, 1.448432226, 1.448361146, 1.448291192, 1.448222337, 1.448154557, 1.448087826, 1.44802212, 1.447957415, 1.447893688, 1.447830919, 1.447769085, 1.447708165, 1.44764814, 1.44758899, 1.447530695, 1.447473238, 1.447416601, 1.447360765, 1.447305715, 1.447251433, 1.447197905, 1.447145113, 1.447093044, 1.447041682, 1.446991013, 1.446941023, 1.446891698, 1.446843026, 1.446794994, 1.446747588, 1.446700797, 1.446654609, 1.446609012, 1.446563996, 1.446519548, 1.446475659, 1.446432318, 1.446389514, 1.446347238, 1.44630548, 1.44626423, 1.44622348, 1.44618322, 1.446143442, 1.446104137, 1.446065296, 1.446026911, 1.445988975, 1.44595148, 1.445914417, 1.44587778, 1.445841561, 1.445805753, 1.445770349, 1.445735343, 1.445700727, 1.445666495, 1.445632641, 1.445599159, 1.445566042, 1.445533284, 1.445500881, 1.445468825, 1.445437112, 1.445405736, 1.445374691, 1.445343973, 1.445313576, 1.445283495, 1.445253726, 1.445224264, 1.445195103, 1.445166239, 1.445137668, 1.445109385, 1.445081387 };
|
||||
private static readonly double[] T_Table_10 = { 6.313751515, 2.91998558, 2.353363435, 2.131846786, 2.015048373, 1.943180281, 1.894578605, 1.859548038, 1.833112933, 1.812461123, 1.795884819, 1.782287556, 1.770933396, 1.761310136, 1.753050356, 1.745883676, 1.739606726, 1.734063607, 1.729132812, 1.724718243, 1.720742903, 1.717144374, 1.713871528, 1.71088208, 1.708140761, 1.70561792, 1.703288446, 1.701130934, 1.699127027, 1.697260887, 1.695518783, 1.693888748, 1.692360309, 1.690924255, 1.689572458, 1.688297714, 1.68709362, 1.68595446, 1.684875122, 1.683851013, 1.682878002, 1.681952357, 1.681070703, 1.680229977, 1.679427393, 1.678660414, 1.677926722, 1.677224196, 1.676550893, 1.675905025, 1.67528495, 1.674689154, 1.674116237, 1.673564906, 1.673033965, 1.672522303, 1.672028888, 1.671552762, 1.671093032, 1.670648865, 1.670219484, 1.669804163, 1.669402222, 1.669013025, 1.668635976, 1.668270514, 1.667916114, 1.667572281, 1.667238549, 1.666914479, 1.666599658, 1.666293696, 1.665996224, 1.665706893, 1.665425373, 1.665151353, 1.664884537, 1.664624645, 1.664371409, 1.664124579, 1.663883913, 1.663649184, 1.663420175, 1.663196679, 1.6629785, 1.662765449, 1.662557349, 1.662354029, 1.662155326, 1.661961084, 1.661771155, 1.661585397, 1.661403674, 1.661225855, 1.661051817, 1.66088144, 1.66071461, 1.660551217, 1.660391156, 1.660234326, 1.66008063, 1.659929976, 1.659782273, 1.659637437, 1.659495383, 1.659356034, 1.659219312, 1.659085144, 1.658953458, 1.658824187, 1.658697265, 1.658572629, 1.658450216, 1.658329969, 1.65821183, 1.658095744, 1.657981659, 1.657869522, 1.657759285, 1.657650899, 1.657544319, 1.657439499, 1.657336397, 1.65723497, 1.657135178, 1.657036982, 1.656940344, 1.656845226, 1.656751594, 1.656659413, 1.656568649, 1.65647927, 1.656391244, 1.656304542, 1.656219133, 1.656134988, 1.65605208, 1.655970382, 1.655889868, 1.655810511, 1.655732287, 1.655655173, 1.655579143, 1.655504177, 1.655430251, 1.655357345, 1.655285437, 1.655214506, 1.655144534, 1.6550755, 1.655007387, 1.654940175, 1.654873847, 1.654808385, 1.654743774, 1.654679996, 1.654617035, 1.654554875, 1.654493503, 1.654432901, 1.654373057, 1.654313957, 1.654255585, 1.654197929, 1.654140976, 1.654084713, 1.654029128, 1.653974208, 1.653919942, 1.653866317, 1.653813324, 1.653760949, 1.653709184, 1.653658017, 1.653607437, 1.653557435, 1.653508002, 1.653459126, 1.6534108, 1.653363013, 1.653315758, 1.653269024, 1.653222803, 1.653177088, 1.653131869, 1.653087138, 1.653042889, 1.652999113, 1.652955802, 1.652912949, 1.652870547, 1.652828589, 1.652787068, 1.652745977, 1.65270531, 1.652665059, 1.652625219, 1.652585784, 1.652546746, 1.652508101 };
|
||||
private static readonly double[] T_Table_05 = { 12.70620474, 4.30265273, 3.182446305, 2.776445105, 2.570581836, 2.446911851, 2.364624252, 2.306004135, 2.262157163, 2.228138852, 2.20098516, 2.17881283, 2.160368656, 2.144786688, 2.131449546, 2.119905299, 2.109815578, 2.10092204, 2.093024054, 2.085963447, 2.079613845, 2.073873068, 2.06865761, 2.063898562, 2.059538553, 2.055529439, 2.051830516, 2.048407142, 2.045229642, 2.042272456, 2.039513446, 2.036933343, 2.034515297, 2.032244509, 2.030107928, 2.028094001, 2.026192463, 2.024394164, 2.02269092, 2.02107539, 2.01954097, 2.018081703, 2.016692199, 2.015367574, 2.014103389, 2.012895599, 2.011740514, 2.010634758, 2.009575237, 2.008559112, 2.00758377, 2.006646805, 2.005745995, 2.004879288, 2.004044783, 2.003240719, 2.002465459, 2.001717484, 2.000995378, 2.000297822, 1.999623585, 1.998971517, 1.998340543, 1.997729654, 1.997137908, 1.996564419, 1.996008354, 1.995468931, 1.994945415, 1.994437112, 1.993943368, 1.993463567, 1.992997126, 1.992543495, 1.992102154, 1.99167261, 1.991254395, 1.990847069, 1.99045021, 1.990063421, 1.989686323, 1.989318557, 1.98895978, 1.988609667, 1.988267907, 1.987934206, 1.987608282, 1.987289865, 1.9869787, 1.986674541, 1.986377154, 1.986086317, 1.985801814, 1.985523442, 1.985251004, 1.984984312, 1.984723186, 1.984467455, 1.984216952, 1.983971519, 1.983731003, 1.983495259, 1.983264145, 1.983037526, 1.982815274, 1.982597262, 1.98238337, 1.982173483, 1.98196749, 1.981765282, 1.981566757, 1.981371815, 1.981180359, 1.980992298, 1.980807541, 1.980626002, 1.980447599, 1.980272249, 1.980099876, 1.979930405, 1.979763763, 1.979599878, 1.979438685, 1.979280117, 1.979124109, 1.978970602, 1.978819535, 1.97867085, 1.978524491, 1.978380405, 1.978238539, 1.978098842, 1.977961264, 1.977825758, 1.977692277, 1.977560777, 1.977431212, 1.977303542, 1.977177724, 1.97705372, 1.976931489, 1.976810994, 1.976692198, 1.976575066, 1.976459563, 1.976345655, 1.976233309, 1.976122494, 1.976013178, 1.975905331, 1.975798924, 1.975693928, 1.975590315, 1.975488058, 1.975387131, 1.975287508, 1.975189163, 1.975092073, 1.974996213, 1.97490156, 1.974808092, 1.974715786, 1.974624621, 1.974534576, 1.97444563, 1.974357764, 1.974270957, 1.974185191, 1.974100447, 1.974016708, 1.973933954, 1.973852169, 1.973771337, 1.97369144, 1.973612462, 1.973534388, 1.973457202, 1.973380889, 1.973305434, 1.973230823, 1.973157042, 1.973084077, 1.973011915, 1.972940542, 1.972869946, 1.972800114, 1.972731033, 1.972662692, 1.972595079, 1.972528182, 1.97246199, 1.972396491, 1.972331676, 1.972267533, 1.972204051, 1.972141222, 1.972079034, 1.972017478, 1.971956544, 1.971896224 };
|
||||
private static readonly double[] T_Table_01 = { 63.65674116, 9.924843201, 5.84090931, 4.604094871, 4.032142984, 3.707428021, 3.499483297, 3.355387331, 3.249835542, 3.169272673, 3.105806516, 3.054539589, 3.012275839, 2.976842734, 2.946712883, 2.920781622, 2.89823052, 2.878440473, 2.860934606, 2.84533971, 2.831359558, 2.818756061, 2.807335684, 2.796939505, 2.787435814, 2.778714533, 2.770682957, 2.763262455, 2.756385904, 2.749995654, 2.744041919, 2.738481482, 2.733276642, 2.728394367, 2.723805589, 2.71948463, 2.715408722, 2.711557602, 2.707913184, 2.704459267, 2.701181304, 2.698066186, 2.695102079, 2.692278266, 2.689585019, 2.687013492, 2.684555618, 2.682204027, 2.679951974, 2.677793271, 2.675722234, 2.673733631, 2.671822636, 2.669984796, 2.668215988, 2.666512398, 2.664870482, 2.663286954, 2.661758752, 2.660283029, 2.658857127, 2.657478565, 2.656145025, 2.654854337, 2.653604469, 2.652393515, 2.651219685, 2.650081299, 2.648976774, 2.647904624, 2.646863444, 2.645851913, 2.644868782, 2.643912872, 2.642983067, 2.642078313, 2.641197611, 2.640340015, 2.639504627, 2.638690596, 2.637897113, 2.63712341, 2.636368757, 2.635632458, 2.634913852, 2.634212309, 2.633527229, 2.632858038, 2.632204191, 2.631565166, 2.630940463, 2.630329608, 2.629732145, 2.629147638, 2.628575671, 2.628015844, 2.627467774, 2.626931096, 2.626405457, 2.625890521, 2.625385965, 2.624891476, 2.624406758, 2.623931523, 2.623465496, 2.623008411, 2.622560015, 2.622120061, 2.621688313, 2.621264543, 2.620848534, 2.620440073, 2.620038957, 2.619644989, 2.619257981, 2.618877749, 2.618504116, 2.618136914, 2.617775976, 2.617421145, 2.617072266, 2.616729191, 2.616391776, 2.616059883, 2.615733377, 2.615412127, 2.615096008, 2.614784899, 2.61447868, 2.614177238, 2.613880461, 2.613588242, 2.613300477, 2.613017065, 2.612737908, 2.61246291, 2.61219198, 2.611925028, 2.611661966, 2.611402711, 2.611147181, 2.610895295, 2.610646976, 2.61040215, 2.610160742, 2.609922682, 2.609687901, 2.609456331, 2.609227907, 2.609002566, 2.608780245, 2.608560883, 2.608344423, 2.608130807, 2.60791998, 2.607711886, 2.607506474, 2.607303692, 2.607103489, 2.606905817, 2.606710628, 2.606517876, 2.606327515, 2.606139501, 2.605953791, 2.605770342, 2.605589114, 2.605410067, 2.605233162, 2.605058359, 2.604885623, 2.604714916, 2.604546204, 2.60437945, 2.604214622, 2.604051686, 2.60389061, 2.603731363, 2.603573912, 2.603418229, 2.603264282, 2.603112045, 2.602961487, 2.602812582, 2.602665303, 2.602519622, 2.602375515, 2.602232955, 2.602091918, 2.60195238, 2.601814317, 2.601677705, 2.601542523, 2.601408747, 2.601276355, 2.601145327, 2.601015642, 2.600887278, 2.600760216, 2.600634436 };
|
||||
}
|
||||
|
||||
public enum Significance
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace AaxDecrypter
|
||||
namespace AaxDecrypter;
|
||||
|
||||
public class MultiConvertFileProperties
|
||||
{
|
||||
public class MultiConvertFileProperties
|
||||
{
|
||||
public required string OutputFileName { get; set; }
|
||||
public int PartsPosition { get; set; }
|
||||
public int PartsTotal { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public DateTime FileDate { get; } = DateTime.Now;
|
||||
}
|
||||
public required string OutputFileName { get; set; }
|
||||
public int PartsPosition { get; set; }
|
||||
public int PartsTotal { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public DateTime FileDate { get; } = DateTime.Now;
|
||||
}
|
||||
|
||||
@@ -171,12 +171,12 @@ public class NetworkFileStream : Stream, IUpdatable
|
||||
|
||||
if (ContentLength != 0 && ContentLength != response.FileSize)
|
||||
throw new WebException($"Content length of 0x{response.FileSize:X10} differs from partially downloaded content length of 0x{ContentLength:X10}");
|
||||
|
||||
|
||||
ContentLength = response.FileSize;
|
||||
|
||||
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
|
||||
//Hand off the client and the open request to the downloader to download and write data to file.
|
||||
DownloadTask = Task.Run(() => DownloadLoopInternal(client , response), _cancellationSource.Token);
|
||||
DownloadTask = Task.Run(() => DownloadLoopInternal(client, response), _cancellationSource.Token);
|
||||
}
|
||||
|
||||
private async Task DownloadLoopInternal(HttpClient client, BlockResponse blockResponse)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,266 +1,267 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
namespace AppScaffolding
|
||||
namespace AppScaffolding;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
///
|
||||
/// directly manipulates settings files without going through domain logic.
|
||||
///
|
||||
/// for migrations only. use with caution.
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
public static class UNSAFE_MigrationHelper
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
///
|
||||
/// directly manipulates settings files without going through domain logic.
|
||||
///
|
||||
/// for migrations only. use with caution.
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
public static class UNSAFE_MigrationHelper
|
||||
public static string? SettingsDirectory
|
||||
=> !APPSETTINGS_TryGet(LibationFiles.LIBATION_FILES_KEY, out var value) || value is null
|
||||
? null
|
||||
: value;
|
||||
|
||||
#region appsettings.json
|
||||
|
||||
public static bool APPSETTINGS_TryGet(string key, out string? value)
|
||||
{
|
||||
public static string? SettingsDirectory
|
||||
=> !APPSETTINGS_TryGet(LibationFiles.LIBATION_FILES_KEY, out var value) || value is null
|
||||
? null
|
||||
: value;
|
||||
bool success = false;
|
||||
JToken? val = null;
|
||||
|
||||
#region appsettings.json
|
||||
process_APPSETTINGS_Json(jObj => success = jObj.TryGetValue(key, out val), false);
|
||||
|
||||
public static bool APPSETTINGS_TryGet(string key, out string? value)
|
||||
value = success ? val?.Value<string>() : null;
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>only insert if not exists</summary>
|
||||
public static void APPSETTINGS_Insert(string key, string value)
|
||||
=> process_APPSETTINGS_Json(jObj => jObj.TryAdd(key, value));
|
||||
|
||||
/// <summary>only update if exists</summary>
|
||||
public static void APPSETTINGS_Update(string key, string value)
|
||||
=> process_APPSETTINGS_Json(jObj =>
|
||||
{
|
||||
bool success = false;
|
||||
JToken? val = null;
|
||||
if (jObj.ContainsKey(key))
|
||||
jObj[key] = value;
|
||||
});
|
||||
|
||||
process_APPSETTINGS_Json(jObj => success = jObj.TryGetValue(key, out val), false);
|
||||
/// <summary>only delete if exists</summary>
|
||||
public static void APPSETTINGS_Delete(string key)
|
||||
=> process_APPSETTINGS_Json(jObj =>
|
||||
{
|
||||
if (jObj.ContainsKey(key))
|
||||
jObj.Remove(key);
|
||||
});
|
||||
|
||||
value = success ? val?.Value<string>() : null;
|
||||
return success;
|
||||
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
|
||||
private static void process_APPSETTINGS_Json(Action<JObject> action, bool save = true)
|
||||
{
|
||||
if (Configuration.Instance.LibationFiles.AppsettingsJsonFile is not string appSettingsFile)
|
||||
return;
|
||||
|
||||
var startingContents = File.ReadAllText(appSettingsFile);
|
||||
|
||||
JObject jObj;
|
||||
try
|
||||
{
|
||||
jObj = JObject.Parse(startingContents);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>only insert if not exists</summary>
|
||||
public static void APPSETTINGS_Insert(string key, string value)
|
||||
=> process_APPSETTINGS_Json(jObj => jObj.TryAdd(key, value));
|
||||
action(jObj);
|
||||
|
||||
/// <summary>only update if exists</summary>
|
||||
public static void APPSETTINGS_Update(string key, string value)
|
||||
=> process_APPSETTINGS_Json(jObj => {
|
||||
if (jObj.ContainsKey(key))
|
||||
jObj[key] = value;
|
||||
});
|
||||
if (!save)
|
||||
return;
|
||||
|
||||
/// <summary>only delete if exists</summary>
|
||||
public static void APPSETTINGS_Delete(string key)
|
||||
=> process_APPSETTINGS_Json(jObj => {
|
||||
if (jObj.ContainsKey(key))
|
||||
jObj.Remove(key);
|
||||
});
|
||||
// only save if different
|
||||
var endingContents_indented = jObj.ToString(Formatting.Indented);
|
||||
var endingContents_compact = jObj.ToString(Formatting.None);
|
||||
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
|
||||
return;
|
||||
|
||||
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
|
||||
private static void process_APPSETTINGS_Json(Action<JObject> action, bool save = true)
|
||||
File.WriteAllText(Configuration.Instance.LibationFiles.AppsettingsJsonFile, endingContents_indented);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
#endregion
|
||||
#region Settings.json
|
||||
|
||||
public static string? SettingsJsonPath => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LibationFiles.SETTINGS_JSON);
|
||||
|
||||
public static bool Settings_TryGet(string key, out string? value)
|
||||
{
|
||||
bool success = false;
|
||||
JToken? val = null;
|
||||
|
||||
process_SettingsJson(jObj => success = jObj.TryGetValue(key, out val), false);
|
||||
|
||||
value = success ? val?.Value<string>() : null;
|
||||
return success;
|
||||
}
|
||||
|
||||
public static bool Settings_JsonPathIsType(string jsonPath, JTokenType jTokenType)
|
||||
{
|
||||
JToken? val = null;
|
||||
|
||||
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
|
||||
|
||||
return val?.Type == jTokenType;
|
||||
}
|
||||
|
||||
public static bool Settings_TryGetFromJsonPath(string jsonPath, out string? value)
|
||||
{
|
||||
JToken? val = null;
|
||||
|
||||
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
|
||||
|
||||
if (val?.Type == JTokenType.String)
|
||||
{
|
||||
if (Configuration.Instance.LibationFiles.AppsettingsJsonFile is not string appSettingsFile)
|
||||
return;
|
||||
|
||||
var startingContents = File.ReadAllText(appSettingsFile);
|
||||
|
||||
JObject jObj;
|
||||
try
|
||||
{
|
||||
jObj = JObject.Parse(startingContents);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
action(jObj);
|
||||
|
||||
if (!save)
|
||||
return;
|
||||
|
||||
// only save if different
|
||||
var endingContents_indented = jObj.ToString(Formatting.Indented);
|
||||
var endingContents_compact = jObj.ToString(Formatting.None);
|
||||
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
|
||||
return;
|
||||
|
||||
File.WriteAllText(Configuration.Instance.LibationFiles.AppsettingsJsonFile, endingContents_indented);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
#endregion
|
||||
#region Settings.json
|
||||
|
||||
public static string? SettingsJsonPath => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LibationFiles.SETTINGS_JSON);
|
||||
|
||||
public static bool Settings_TryGet(string key, out string? value)
|
||||
{
|
||||
bool success = false;
|
||||
JToken? val = null;
|
||||
|
||||
process_SettingsJson(jObj => success = jObj.TryGetValue(key, out val), false);
|
||||
|
||||
value = success ? val?.Value<string>() : null;
|
||||
return success;
|
||||
}
|
||||
|
||||
public static bool Settings_JsonPathIsType(string jsonPath, JTokenType jTokenType)
|
||||
{
|
||||
JToken? val = null;
|
||||
|
||||
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
|
||||
|
||||
return val?.Type == jTokenType;
|
||||
}
|
||||
|
||||
public static bool Settings_TryGetFromJsonPath(string jsonPath, out string? value)
|
||||
{
|
||||
JToken? val = null;
|
||||
|
||||
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
|
||||
|
||||
if (val?.Type == JTokenType.String)
|
||||
{
|
||||
value = val.Value<string>();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Settings_SetWithJsonPath(string jsonPath, string propertyName, string newValue)
|
||||
{
|
||||
if (!Settings_TryGetFromJsonPath($"{jsonPath}.{propertyName}", out _))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
var token = jObj.SelectToken(jsonPath);
|
||||
if (token is null
|
||||
|| token is not JObject o
|
||||
|| o[propertyName] is null)
|
||||
return;
|
||||
|
||||
var oldValue = token.Value<string>(propertyName);
|
||||
if (oldValue != newValue)
|
||||
token[propertyName] = newValue;
|
||||
});
|
||||
}
|
||||
|
||||
public static bool Settings_TryGetArrayLength(string jsonPath, out int length)
|
||||
{
|
||||
length = 0;
|
||||
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return false;
|
||||
|
||||
JArray? array = null;
|
||||
process_SettingsJson(jObj => array = jObj.SelectToken(jsonPath) as JArray);
|
||||
|
||||
length = array?.Count ?? 0;
|
||||
value = val.Value<string>();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Settings_AddToArray(string jsonPath, string newValue)
|
||||
else
|
||||
{
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
(jObj.SelectToken(jsonPath) as JArray)?.Add(newValue);
|
||||
});
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Do not add if already exists</summary>
|
||||
public static void Settings_AddUniqueToArray(string arrayPath, string newValue)
|
||||
{
|
||||
if (!Settings_TryGetArrayLength(arrayPath, out var qty))
|
||||
return;
|
||||
|
||||
for (var i = 0; i < qty; i++)
|
||||
{
|
||||
var exists = Settings_TryGetFromJsonPath($"{arrayPath}[{i}]", out var value);
|
||||
if (exists && value == newValue)
|
||||
return;
|
||||
}
|
||||
|
||||
Settings_AddToArray(arrayPath, newValue);
|
||||
}
|
||||
|
||||
/// <summary>only remove if not exists</summary>
|
||||
public static void Settings_RemoveFromArray(string jsonPath, int position)
|
||||
{
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
if (jObj.SelectToken(jsonPath) is JArray array && position < array.Count)
|
||||
array.RemoveAt(position);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>only insert if not exists</summary>
|
||||
public static void Settings_Insert(string key, string value)
|
||||
=> process_SettingsJson(jObj => jObj.TryAdd(key, value));
|
||||
|
||||
/// <summary>only update if exists</summary>
|
||||
public static void Settings_Update(string key, string value)
|
||||
=> process_SettingsJson(jObj => {
|
||||
if (jObj.ContainsKey(key))
|
||||
jObj[key] = value;
|
||||
});
|
||||
|
||||
/// <summary>only delete if exists</summary>
|
||||
public static void Settings_Delete(string key)
|
||||
=> process_SettingsJson(jObj => {
|
||||
if (jObj.ContainsKey(key))
|
||||
jObj.Remove(key);
|
||||
});
|
||||
|
||||
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
|
||||
private static void process_SettingsJson(Action<JObject> action, bool save = true)
|
||||
{
|
||||
// only insert if not exists
|
||||
if (!File.Exists(SettingsJsonPath))
|
||||
return;
|
||||
|
||||
var startingContents = File.ReadAllText(SettingsJsonPath);
|
||||
|
||||
JObject jObj;
|
||||
try
|
||||
{
|
||||
jObj = JObject.Parse(startingContents);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
action(jObj);
|
||||
|
||||
if (!save)
|
||||
return;
|
||||
|
||||
// only save if different
|
||||
var endingContents_indented = jObj.ToString(Formatting.Indented);
|
||||
var endingContents_compact = jObj.ToString(Formatting.None);
|
||||
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
|
||||
return;
|
||||
|
||||
File.WriteAllText(SettingsJsonPath, endingContents_indented);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
#endregion
|
||||
#region LibationContext.db
|
||||
public const string LIBATION_CONTEXT = "LibationContext.db";
|
||||
public static string? DatabaseFile => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LIBATION_CONTEXT);
|
||||
public static bool DatabaseFile_Exists => DatabaseFile is not null && File.Exists(DatabaseFile);
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static void Settings_SetWithJsonPath(string jsonPath, string propertyName, string newValue)
|
||||
{
|
||||
if (!Settings_TryGetFromJsonPath($"{jsonPath}.{propertyName}", out _))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
var token = jObj.SelectToken(jsonPath);
|
||||
if (token is null
|
||||
|| token is not JObject o
|
||||
|| o[propertyName] is null)
|
||||
return;
|
||||
|
||||
var oldValue = token.Value<string>(propertyName);
|
||||
if (oldValue != newValue)
|
||||
token[propertyName] = newValue;
|
||||
});
|
||||
}
|
||||
|
||||
public static bool Settings_TryGetArrayLength(string jsonPath, out int length)
|
||||
{
|
||||
length = 0;
|
||||
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return false;
|
||||
|
||||
JArray? array = null;
|
||||
process_SettingsJson(jObj => array = jObj.SelectToken(jsonPath) as JArray);
|
||||
|
||||
length = array?.Count ?? 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Settings_AddToArray(string jsonPath, string newValue)
|
||||
{
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
(jObj.SelectToken(jsonPath) as JArray)?.Add(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Do not add if already exists</summary>
|
||||
public static void Settings_AddUniqueToArray(string arrayPath, string newValue)
|
||||
{
|
||||
if (!Settings_TryGetArrayLength(arrayPath, out var qty))
|
||||
return;
|
||||
|
||||
for (var i = 0; i < qty; i++)
|
||||
{
|
||||
var exists = Settings_TryGetFromJsonPath($"{arrayPath}[{i}]", out var value);
|
||||
if (exists && value == newValue)
|
||||
return;
|
||||
}
|
||||
|
||||
Settings_AddToArray(arrayPath, newValue);
|
||||
}
|
||||
|
||||
/// <summary>only remove if not exists</summary>
|
||||
public static void Settings_RemoveFromArray(string jsonPath, int position)
|
||||
{
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
if (jObj.SelectToken(jsonPath) is JArray array && position < array.Count)
|
||||
array.RemoveAt(position);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>only insert if not exists</summary>
|
||||
public static void Settings_Insert(string key, string value)
|
||||
=> process_SettingsJson(jObj => jObj.TryAdd(key, value));
|
||||
|
||||
/// <summary>only update if exists</summary>
|
||||
public static void Settings_Update(string key, string value)
|
||||
=> process_SettingsJson(jObj =>
|
||||
{
|
||||
if (jObj.ContainsKey(key))
|
||||
jObj[key] = value;
|
||||
});
|
||||
|
||||
/// <summary>only delete if exists</summary>
|
||||
public static void Settings_Delete(string key)
|
||||
=> process_SettingsJson(jObj =>
|
||||
{
|
||||
if (jObj.ContainsKey(key))
|
||||
jObj.Remove(key);
|
||||
});
|
||||
|
||||
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
|
||||
private static void process_SettingsJson(Action<JObject> action, bool save = true)
|
||||
{
|
||||
// only insert if not exists
|
||||
if (!File.Exists(SettingsJsonPath))
|
||||
return;
|
||||
|
||||
var startingContents = File.ReadAllText(SettingsJsonPath);
|
||||
|
||||
JObject jObj;
|
||||
try
|
||||
{
|
||||
jObj = JObject.Parse(startingContents);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
action(jObj);
|
||||
|
||||
if (!save)
|
||||
return;
|
||||
|
||||
// only save if different
|
||||
var endingContents_indented = jObj.ToString(Formatting.Indented);
|
||||
var endingContents_compact = jObj.ToString(Formatting.None);
|
||||
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
|
||||
return;
|
||||
|
||||
File.WriteAllText(SettingsJsonPath, endingContents_indented);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
#endregion
|
||||
#region LibationContext.db
|
||||
public const string LIBATION_CONTEXT = "LibationContext.db";
|
||||
public static string? DatabaseFile => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LIBATION_CONTEXT);
|
||||
public static bool DatabaseFile_Exists => DatabaseFile is not null && File.Exists(DatabaseFile);
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AppScaffolding
|
||||
namespace AppScaffolding;
|
||||
|
||||
public partial record UpgradeProperties
|
||||
{
|
||||
public partial record UpgradeProperties
|
||||
public string ZipUrl { get; }
|
||||
public string HtmlUrl { get; }
|
||||
public string ZipName { get; }
|
||||
public Version LatestRelease { get; }
|
||||
public string Notes { get; }
|
||||
|
||||
public UpgradeProperties(string zipUrl, string htmlUrl, string zipName, Version latestRelease, string notes)
|
||||
{
|
||||
public string ZipUrl { get; }
|
||||
public string HtmlUrl { get; }
|
||||
public string ZipName { get; }
|
||||
public Version LatestRelease { get; }
|
||||
public string Notes { get; }
|
||||
|
||||
public UpgradeProperties(string zipUrl, string htmlUrl, string zipName, Version latestRelease, string notes)
|
||||
{
|
||||
ZipName = zipName;
|
||||
HtmlUrl = htmlUrl;
|
||||
ZipUrl = zipUrl;
|
||||
LatestRelease = latestRelease;
|
||||
Notes = LinkStripRegex().Replace(notes, "$1");
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\[(.*)\]\(.*\)")]
|
||||
private static partial Regex LinkStripRegex();
|
||||
ZipName = zipName;
|
||||
HtmlUrl = htmlUrl;
|
||||
ZipUrl = zipUrl;
|
||||
LatestRelease = latestRelease;
|
||||
Notes = LinkStripRegex().Replace(notes, "$1");
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\[(.*)\]\(.*\)")]
|
||||
private static partial Regex LinkStripRegex();
|
||||
}
|
||||
|
||||
@@ -1,78 +1,76 @@
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace ApplicationServices
|
||||
namespace ApplicationServices;
|
||||
|
||||
public class BulkSetDownloadStatus
|
||||
{
|
||||
public class BulkSetDownloadStatus
|
||||
{
|
||||
private List<(string message, LiberatedStatus newStatus, IEnumerable<LibraryBook> LibraryBooks)> actionSets { get; } = new();
|
||||
private List<(string message, LiberatedStatus newStatus, IEnumerable<LibraryBook> LibraryBooks)> actionSets { get; } = new();
|
||||
|
||||
public int Count => actionSets.Count;
|
||||
public int Count => actionSets.Count;
|
||||
|
||||
public IEnumerable<string> Messages => actionSets.Select(a => a.message);
|
||||
public string AggregateMessage => $"Are you sure you want to set {Messages.Aggregate((a, b) => $"{a} and {b}")}?";
|
||||
public IEnumerable<string> Messages => actionSets.Select(a => a.message);
|
||||
public string AggregateMessage => $"Are you sure you want to set {Messages.Aggregate((a, b) => $"{a} and {b}")}?";
|
||||
|
||||
private List<LibraryBook> _libraryBooks;
|
||||
private bool _setDownloaded;
|
||||
private bool _setNotDownloaded;
|
||||
private readonly List<LibraryBook> _libraryBooks;
|
||||
private readonly bool _setDownloaded;
|
||||
private readonly bool _setNotDownloaded;
|
||||
|
||||
public BulkSetDownloadStatus(List<LibraryBook> libraryBooks, bool setDownloaded, bool setNotDownloaded)
|
||||
{
|
||||
_libraryBooks = libraryBooks;
|
||||
_setDownloaded = setDownloaded;
|
||||
_setNotDownloaded = setNotDownloaded;
|
||||
}
|
||||
public BulkSetDownloadStatus(List<LibraryBook> libraryBooks, bool setDownloaded, bool setNotDownloaded)
|
||||
{
|
||||
_libraryBooks = libraryBooks;
|
||||
_setDownloaded = setDownloaded;
|
||||
_setNotDownloaded = setNotDownloaded;
|
||||
}
|
||||
|
||||
public int Discover()
|
||||
{
|
||||
var bookExistsList = _libraryBooks
|
||||
.Select(libraryBook => new
|
||||
{
|
||||
LibraryBook = libraryBook,
|
||||
FileExists = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId) is not null
|
||||
})
|
||||
.ToList();
|
||||
public int Discover()
|
||||
{
|
||||
var bookExistsList = _libraryBooks
|
||||
.Select(libraryBook => new
|
||||
{
|
||||
LibraryBook = libraryBook,
|
||||
FileExists = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId) is not null
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (_setDownloaded)
|
||||
{
|
||||
var books2change = bookExistsList
|
||||
.Where(a => a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
|
||||
.Select(a => a.LibraryBook)
|
||||
.ToList();
|
||||
if (_setDownloaded)
|
||||
{
|
||||
var books2change = bookExistsList
|
||||
.Where(a => a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
|
||||
.Select(a => a.LibraryBook)
|
||||
.ToList();
|
||||
|
||||
if (books2change.Any())
|
||||
actionSets.Add((
|
||||
$"{"book".PluralizeWithCount(books2change.Count)} to 'Downloaded'",
|
||||
LiberatedStatus.Liberated,
|
||||
books2change));
|
||||
}
|
||||
if (books2change.Any())
|
||||
actionSets.Add((
|
||||
$"{"book".PluralizeWithCount(books2change.Count)} to 'Downloaded'",
|
||||
LiberatedStatus.Liberated,
|
||||
books2change));
|
||||
}
|
||||
|
||||
if (_setNotDownloaded)
|
||||
{
|
||||
var books2change = bookExistsList
|
||||
.Where(a => !a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated)
|
||||
.Select(a => a.LibraryBook)
|
||||
.ToList();
|
||||
if (_setNotDownloaded)
|
||||
{
|
||||
var books2change = bookExistsList
|
||||
.Where(a => !a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated)
|
||||
.Select(a => a.LibraryBook)
|
||||
.ToList();
|
||||
|
||||
if (books2change.Any())
|
||||
actionSets.Add((
|
||||
$"{"book".PluralizeWithCount(books2change.Count)} to 'Not Downloaded'",
|
||||
LiberatedStatus.NotLiberated,
|
||||
books2change));
|
||||
}
|
||||
if (books2change.Any())
|
||||
actionSets.Add((
|
||||
$"{"book".PluralizeWithCount(books2change.Count)} to 'Not Downloaded'",
|
||||
LiberatedStatus.NotLiberated,
|
||||
books2change));
|
||||
}
|
||||
|
||||
return Count;
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
foreach (var a in actionSets)
|
||||
await a.LibraryBooks.UpdateBookStatusAsync(a.newStatus);
|
||||
}
|
||||
}
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
foreach (var a in actionSets)
|
||||
await a.LibraryBooks.UpdateBookStatusAsync(a.newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,43 +3,42 @@ using LibationFileManager;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ApplicationServices
|
||||
namespace ApplicationServices;
|
||||
|
||||
public static class DbContexts
|
||||
{
|
||||
public static class DbContexts
|
||||
{
|
||||
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
|
||||
public static LibationContext GetContext()
|
||||
{
|
||||
var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString)
|
||||
? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString)
|
||||
: LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString);
|
||||
context.Database.Migrate();
|
||||
return context;
|
||||
}
|
||||
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
|
||||
public static LibationContext GetContext()
|
||||
{
|
||||
var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString)
|
||||
? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString)
|
||||
: LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString);
|
||||
context.Database.Migrate();
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>Use for full library querying. No lazy loading</summary>
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetLibrary_Flat_NoTracking(includeParents);
|
||||
}
|
||||
/// <summary>Use for full library querying. No lazy loading</summary>
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetLibrary_Flat_NoTracking(includeParents);
|
||||
}
|
||||
|
||||
public static List<LibraryBook> GetUnliberated_Flat_NoTracking()
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetUnLiberated_Flat_NoTracking();
|
||||
}
|
||||
public static List<LibraryBook> GetUnliberated_Flat_NoTracking()
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetUnLiberated_Flat_NoTracking();
|
||||
}
|
||||
|
||||
public static List<LibraryBook> GetDeletedLibraryBooks()
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetDeletedLibraryBooks();
|
||||
}
|
||||
public static List<LibraryBook> GetDeletedLibraryBooks()
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetDeletedLibraryBooks();
|
||||
}
|
||||
|
||||
public static LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true)
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetLibraryBook_Flat_NoTracking(productId, caseSensative);
|
||||
}
|
||||
public static LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true)
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetLibraryBook_Flat_NoTracking(productId, caseSensative);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,8 @@ public static class LibraryExporter
|
||||
using var csv = new CsvWriter(new System.IO.StreamWriter(saveFilePath), CultureInfo.CurrentCulture);
|
||||
csv.WriteHeader(typeof(ExportDto));
|
||||
csv.NextRecord();
|
||||
csv.WriteRecords(dtos); }
|
||||
csv.WriteRecords(dtos);
|
||||
}
|
||||
|
||||
public static void ToJson(string saveFilePath, IEnumerable<LibraryBook>? libraryBooks = null)
|
||||
{
|
||||
|
||||
@@ -8,179 +8,178 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace ApplicationServices
|
||||
namespace ApplicationServices;
|
||||
|
||||
public static class RecordExporter
|
||||
{
|
||||
public static class RecordExporter
|
||||
public static void ToXlsx(string saveFilePath, IEnumerable<IRecord> records)
|
||||
{
|
||||
public static void ToXlsx(string saveFilePath, IEnumerable<IRecord> records)
|
||||
if (!records.Any())
|
||||
return;
|
||||
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.AddWorksheet("Records");
|
||||
|
||||
// headers
|
||||
var columns = new List<string>
|
||||
{
|
||||
if (!records.Any())
|
||||
return;
|
||||
nameof(Type.Name),
|
||||
nameof(IRecord.Created),
|
||||
nameof(IRecord.Start) + "_ms",
|
||||
};
|
||||
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.AddWorksheet("Records");
|
||||
if (records.OfType<IAnnotation>().Any())
|
||||
{
|
||||
columns.Add(nameof(IAnnotation.AnnotationId));
|
||||
columns.Add(nameof(IAnnotation.LastModified));
|
||||
}
|
||||
if (records.OfType<IRangeAnnotation>().Any())
|
||||
{
|
||||
columns.Add(nameof(IRangeAnnotation.End) + "_ms");
|
||||
columns.Add(nameof(IRangeAnnotation.Text));
|
||||
}
|
||||
if (records.OfType<Clip>().Any())
|
||||
columns.Add(nameof(Clip.Title));
|
||||
|
||||
// headers
|
||||
var columns = new List<string>
|
||||
int rowIndex = 1, col = 1;
|
||||
var headerRow = worksheet.Row(rowIndex++);
|
||||
foreach (var c in columns)
|
||||
{
|
||||
var headerCell = headerRow.Cell(col++);
|
||||
headerCell.Value = c;
|
||||
headerCell.Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
|
||||
|
||||
// Add data rows
|
||||
foreach (var record in records)
|
||||
{
|
||||
col = 1;
|
||||
var row = worksheet.Row(rowIndex++);
|
||||
|
||||
row.Cell(col++).Value = record.GetType().Name;
|
||||
row.Cell(col++).SetDate(record.Created.DateTime, dateFormat);
|
||||
row.Cell(col++).Value = record.Start.TotalMilliseconds;
|
||||
|
||||
if (record is IAnnotation annotation)
|
||||
{
|
||||
nameof(Type.Name),
|
||||
nameof(IRecord.Created),
|
||||
nameof(IRecord.Start) + "_ms",
|
||||
};
|
||||
|
||||
if (records.OfType<IAnnotation>().Any())
|
||||
{
|
||||
columns.Add(nameof(IAnnotation.AnnotationId));
|
||||
columns.Add(nameof(IAnnotation.LastModified));
|
||||
}
|
||||
if (records.OfType<IRangeAnnotation>().Any())
|
||||
{
|
||||
columns.Add(nameof(IRangeAnnotation.End) + "_ms");
|
||||
columns.Add(nameof(IRangeAnnotation.Text));
|
||||
}
|
||||
if (records.OfType<Clip>().Any())
|
||||
columns.Add(nameof(Clip.Title));
|
||||
row.Cell(col++).Value = annotation.AnnotationId;
|
||||
row.Cell(col++).SetDate(annotation.LastModified.DateTime, dateFormat);
|
||||
|
||||
int rowIndex = 1, col = 1;
|
||||
var headerRow = worksheet.Row(rowIndex++);
|
||||
foreach (var c in columns)
|
||||
{
|
||||
var headerCell = headerRow.Cell(col++);
|
||||
headerCell.Value = c;
|
||||
headerCell.Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
|
||||
|
||||
// Add data rows
|
||||
foreach (var record in records)
|
||||
{
|
||||
col = 1;
|
||||
var row = worksheet.Row(rowIndex++);
|
||||
|
||||
row.Cell(col++).Value = record.GetType().Name;
|
||||
row.Cell(col++).SetDate(record.Created.DateTime, dateFormat);
|
||||
row.Cell(col++).Value = record.Start.TotalMilliseconds;
|
||||
|
||||
if (record is IAnnotation annotation)
|
||||
if (annotation is IRangeAnnotation rangeAnnotation)
|
||||
{
|
||||
row.Cell(col++).Value = rangeAnnotation.End.TotalMilliseconds;
|
||||
row.Cell(col++).Value = rangeAnnotation.Text;
|
||||
|
||||
row.Cell(col++).Value = annotation.AnnotationId;
|
||||
row.Cell(col++).SetDate(annotation.LastModified.DateTime, dateFormat);
|
||||
|
||||
if (annotation is IRangeAnnotation rangeAnnotation)
|
||||
{
|
||||
row.Cell(col++).Value = rangeAnnotation.End.TotalMilliseconds;
|
||||
row.Cell(col++).Value = rangeAnnotation.Text;
|
||||
|
||||
if (rangeAnnotation is Clip clip)
|
||||
row.Cell(col++).Value = clip.Title;
|
||||
}
|
||||
if (rangeAnnotation is Clip clip)
|
||||
row.Cell(col++).Value = clip.Title;
|
||||
}
|
||||
}
|
||||
|
||||
workbook.SaveAs(saveFilePath);
|
||||
}
|
||||
|
||||
private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat)
|
||||
{
|
||||
cell.Value = value;
|
||||
cell.Style.DateFormat.Format = dateFormat;
|
||||
}
|
||||
public static void ToJson(string saveFilePath, LibraryBook libraryBook, IEnumerable<IRecord> records)
|
||||
{
|
||||
if (!records.Any())
|
||||
return;
|
||||
workbook.SaveAs(saveFilePath);
|
||||
}
|
||||
|
||||
var recordsEx = extendRecords(records);
|
||||
private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat)
|
||||
{
|
||||
cell.Value = value;
|
||||
cell.Style.DateFormat.Format = dateFormat;
|
||||
}
|
||||
public static void ToJson(string saveFilePath, LibraryBook libraryBook, IEnumerable<IRecord> records)
|
||||
{
|
||||
if (!records.Any())
|
||||
return;
|
||||
|
||||
var recordsObj = new JObject
|
||||
var recordsEx = extendRecords(records);
|
||||
|
||||
var recordsObj = new JObject
|
||||
{
|
||||
{ "title", libraryBook.Book.TitleWithSubtitle},
|
||||
{ "asin", libraryBook.Book.AudibleProductId},
|
||||
{ "exportTime", DateTime.Now},
|
||||
{ "records", JArray.FromObject(recordsEx) }
|
||||
};
|
||||
|
||||
System.IO.File.WriteAllText(saveFilePath, recordsObj.ToString(Newtonsoft.Json.Formatting.Indented));
|
||||
}
|
||||
|
||||
public static void ToCsv(string saveFilePath, IEnumerable<IRecord> records)
|
||||
{
|
||||
if (!records.Any())
|
||||
return;
|
||||
|
||||
using var writer = new System.IO.StreamWriter(saveFilePath);
|
||||
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
|
||||
|
||||
//Write headers for the present record type that has the most properties
|
||||
if (records.OfType<Clip>().Any())
|
||||
csv.WriteHeader(typeof(ClipEx));
|
||||
else if (records.OfType<Note>().Any())
|
||||
csv.WriteHeader(typeof(NoteEx));
|
||||
else if (records.OfType<Bookmark>().Any())
|
||||
csv.WriteHeader(typeof(BookmarkEx));
|
||||
else
|
||||
csv.WriteHeader(typeof(LastHeardEx));
|
||||
|
||||
var recordsEx = extendRecords(records);
|
||||
|
||||
csv.NextRecord();
|
||||
csv.WriteRecords(recordsEx.OfType<ClipEx>());
|
||||
csv.WriteRecords(recordsEx.OfType<NoteEx>());
|
||||
csv.WriteRecords(recordsEx.OfType<BookmarkEx>());
|
||||
csv.WriteRecords(recordsEx.OfType<LastHeardEx>());
|
||||
}
|
||||
|
||||
private static IEnumerable<IRecordEx> extendRecords(IEnumerable<IRecord> records)
|
||||
=> records
|
||||
.Select<IRecord, IRecordEx>(
|
||||
r => r switch
|
||||
{
|
||||
{ "title", libraryBook.Book.TitleWithSubtitle},
|
||||
{ "asin", libraryBook.Book.AudibleProductId},
|
||||
{ "exportTime", DateTime.Now},
|
||||
{ "records", JArray.FromObject(recordsEx) }
|
||||
};
|
||||
Clip c => new ClipEx(nameof(Clip), c),
|
||||
Note n => new NoteEx(nameof(Note), n),
|
||||
Bookmark b => new BookmarkEx(nameof(Bookmark), b),
|
||||
LastHeard l => new LastHeardEx(nameof(LastHeard), l),
|
||||
_ => throw new InvalidOperationException(),
|
||||
});
|
||||
|
||||
System.IO.File.WriteAllText(saveFilePath, recordsObj.ToString(Newtonsoft.Json.Formatting.Indented));
|
||||
}
|
||||
|
||||
public static void ToCsv(string saveFilePath, IEnumerable<IRecord> records)
|
||||
private interface IRecordEx { string Type { get; } }
|
||||
|
||||
private record LastHeardEx : LastHeard, IRecordEx
|
||||
{
|
||||
public string Type { get; }
|
||||
public LastHeardEx(string type, LastHeard original) : base(original)
|
||||
{
|
||||
if (!records.Any())
|
||||
return;
|
||||
|
||||
using var writer = new System.IO.StreamWriter(saveFilePath);
|
||||
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
|
||||
|
||||
//Write headers for the present record type that has the most properties
|
||||
if (records.OfType<Clip>().Any())
|
||||
csv.WriteHeader(typeof(ClipEx));
|
||||
else if (records.OfType<Note>().Any())
|
||||
csv.WriteHeader(typeof(NoteEx));
|
||||
else if (records.OfType<Bookmark>().Any())
|
||||
csv.WriteHeader(typeof(BookmarkEx));
|
||||
else
|
||||
csv.WriteHeader(typeof(LastHeardEx));
|
||||
|
||||
var recordsEx = extendRecords(records);
|
||||
|
||||
csv.NextRecord();
|
||||
csv.WriteRecords(recordsEx.OfType<ClipEx>());
|
||||
csv.WriteRecords(recordsEx.OfType<NoteEx>());
|
||||
csv.WriteRecords(recordsEx.OfType<BookmarkEx>());
|
||||
csv.WriteRecords(recordsEx.OfType<LastHeardEx>());
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<IRecordEx> extendRecords(IEnumerable<IRecord> records)
|
||||
=> records
|
||||
.Select<IRecord, IRecordEx>(
|
||||
r => r switch
|
||||
{
|
||||
Clip c => new ClipEx(nameof(Clip), c),
|
||||
Note n => new NoteEx(nameof(Note), n),
|
||||
Bookmark b => new BookmarkEx(nameof(Bookmark), b),
|
||||
LastHeard l => new LastHeardEx(nameof(LastHeard), l),
|
||||
_ => throw new InvalidOperationException(),
|
||||
});
|
||||
|
||||
|
||||
private interface IRecordEx { string Type { get; } }
|
||||
|
||||
private record LastHeardEx : LastHeard, IRecordEx
|
||||
private record BookmarkEx : Bookmark, IRecordEx
|
||||
{
|
||||
public string Type { get; }
|
||||
public BookmarkEx(string type, Bookmark original) : base(original)
|
||||
{
|
||||
public string Type { get; }
|
||||
public LastHeardEx(string type, LastHeard original) : base(original)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
|
||||
private record BookmarkEx : Bookmark, IRecordEx
|
||||
private record NoteEx : Note, IRecordEx
|
||||
{
|
||||
public string Type { get; }
|
||||
public NoteEx(string type, Note original) : base(original)
|
||||
{
|
||||
public string Type { get; }
|
||||
public BookmarkEx(string type, Bookmark original) : base(original)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
|
||||
private record NoteEx : Note, IRecordEx
|
||||
private record ClipEx : Clip, IRecordEx
|
||||
{
|
||||
public string Type { get; }
|
||||
public ClipEx(string type, Clip original) : base(original)
|
||||
{
|
||||
public string Type { get; }
|
||||
public NoteEx(string type, Note original) : base(original)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
|
||||
private record ClipEx : Clip, IRecordEx
|
||||
{
|
||||
public string Type { get; }
|
||||
public ClipEx(string type, Clip original) : base(original)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +1,105 @@
|
||||
using System;
|
||||
using DataLayer;
|
||||
using LibationSearchEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
using LibationSearchEngine;
|
||||
|
||||
namespace ApplicationServices
|
||||
namespace ApplicationServices;
|
||||
|
||||
public static class SearchEngineCommands
|
||||
{
|
||||
public static class SearchEngineCommands
|
||||
#region Search
|
||||
public static SearchResultSet Search(string searchString) => performSafeQuery(e =>
|
||||
e.Search(searchString)
|
||||
);
|
||||
|
||||
private static T performSafeQuery<T>(Func<SearchEngine, T> func)
|
||||
{
|
||||
#region Search
|
||||
public static SearchResultSet Search(string searchString) => performSafeQuery(e =>
|
||||
e.Search(searchString)
|
||||
);
|
||||
|
||||
private static T performSafeQuery<T>(Func<SearchEngine, T> func)
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
return func(engine);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
fullReIndex(engine);
|
||||
return func(engine);
|
||||
}
|
||||
return func(engine);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static event EventHandler? SearchEngineUpdated;
|
||||
|
||||
#region Update
|
||||
private static bool isUpdating;
|
||||
|
||||
public static void UpdateBooks(IEnumerable<LibraryBook> books)
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
|
||||
// I did not benchmark before choosing the number here
|
||||
if (books.Count() > 15)
|
||||
FullReIndex();
|
||||
else
|
||||
{
|
||||
foreach (var book in books)
|
||||
UpdateUserDefinedItems(book);
|
||||
}
|
||||
fullReIndex(engine);
|
||||
return func(engine);
|
||||
}
|
||||
|
||||
public static void FullReIndex() => performSafeCommand(fullReIndex);
|
||||
public static void FullReIndex(List<LibraryBook> libraryBooks)
|
||||
=> performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents()));
|
||||
|
||||
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
|
||||
{
|
||||
e.UpdateLiberatedStatus(book);
|
||||
e.UpdateTags(book.Book.AudibleProductId, book.Book.UserDefinedItem.Tags);
|
||||
e.UpdateUserRatings(book);
|
||||
}
|
||||
);
|
||||
|
||||
private static void performSafeCommand(Action<SearchEngine> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
update(action);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
fullReIndex(new SearchEngine());
|
||||
update(action);
|
||||
}
|
||||
}
|
||||
|
||||
private static void update(Action<SearchEngine> action)
|
||||
{
|
||||
if (action is null)
|
||||
return;
|
||||
|
||||
// support nesting incl recursion
|
||||
var prevIsUpdating = isUpdating;
|
||||
try
|
||||
{
|
||||
isUpdating = true;
|
||||
|
||||
action(new SearchEngine());
|
||||
if (!prevIsUpdating)
|
||||
SearchEngineUpdated?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isUpdating = prevIsUpdating;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fullReIndex(SearchEngine engine)
|
||||
{
|
||||
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
fullReIndex(engine, library);
|
||||
}
|
||||
|
||||
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
|
||||
=> engine.CreateNewIndex(libraryBooks);
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static event EventHandler? SearchEngineUpdated;
|
||||
|
||||
#region Update
|
||||
private static bool isUpdating;
|
||||
|
||||
public static void UpdateBooks(IEnumerable<LibraryBook> books)
|
||||
{
|
||||
// Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
|
||||
// I did not benchmark before choosing the number here
|
||||
if (books.Count() > 15)
|
||||
FullReIndex();
|
||||
else
|
||||
{
|
||||
foreach (var book in books)
|
||||
UpdateUserDefinedItems(book);
|
||||
}
|
||||
}
|
||||
|
||||
public static void FullReIndex() => performSafeCommand(fullReIndex);
|
||||
public static void FullReIndex(List<LibraryBook> libraryBooks)
|
||||
=> performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents()));
|
||||
|
||||
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
|
||||
{
|
||||
e.UpdateLiberatedStatus(book);
|
||||
e.UpdateTags(book.Book.AudibleProductId, book.Book.UserDefinedItem.Tags);
|
||||
e.UpdateUserRatings(book);
|
||||
}
|
||||
);
|
||||
|
||||
private static void performSafeCommand(Action<SearchEngine> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
update(action);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
fullReIndex(new SearchEngine());
|
||||
update(action);
|
||||
}
|
||||
}
|
||||
|
||||
private static void update(Action<SearchEngine> action)
|
||||
{
|
||||
if (action is null)
|
||||
return;
|
||||
|
||||
// support nesting incl recursion
|
||||
var prevIsUpdating = isUpdating;
|
||||
try
|
||||
{
|
||||
isUpdating = true;
|
||||
|
||||
action(new SearchEngine());
|
||||
if (!prevIsUpdating)
|
||||
SearchEngineUpdated?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isUpdating = prevIsUpdating;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fullReIndex(SearchEngine engine)
|
||||
{
|
||||
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
fullReIndex(engine, library);
|
||||
}
|
||||
|
||||
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
|
||||
=> engine.CreateNewIndex(libraryBooks);
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using AudibleApi;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Authorization;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace AudibleUtilities;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Authorization;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AudibleUtilities;
|
||||
|
||||
@@ -30,7 +30,7 @@ public class AccountsSettings : IUpdatable
|
||||
protected AccountsSettings(List<Account> accountsSettings) { }
|
||||
|
||||
#region Accounts
|
||||
private List<Account> _accounts_backing = new List<Account>();
|
||||
private readonly List<Account> _accounts_backing = new List<Account>();
|
||||
[JsonProperty(PropertyName = nameof(Accounts))]
|
||||
private List<Account> _accounts_json
|
||||
{
|
||||
@@ -150,7 +150,7 @@ public class AccountsSettings : IUpdatable
|
||||
// same account instance: ok
|
||||
if (acct == account)
|
||||
return;
|
||||
|
||||
|
||||
// same account id + locale, different instance: bad
|
||||
throw new InvalidOperationException("Cannot add an account with the same account Id and Locale");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleApi.Authorization;
|
||||
using Dinah.Core.IO;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
namespace AudibleUtilities;
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ public class ApiExtended
|
||||
|
||||
return new ApiExtended(api);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AsyncRetryPolicy policy { get; }
|
||||
= Policy.Handle<Exception>()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace AudibleUtilities;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using AudibleApi.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
|
||||
namespace AudibleUtilities;
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleApi.Cryptography;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AudibleUtilities;
|
||||
|
||||
@@ -15,7 +15,7 @@ public partial class Mkb79Auth : IIdentityMaintainer
|
||||
{
|
||||
[JsonProperty("website_cookies")]
|
||||
private JObject? _websiteCookies { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("adp_token")]
|
||||
public string? AdpToken { get; private set; }
|
||||
|
||||
@@ -83,7 +83,7 @@ public partial class Mkb79Auth : IIdentityMaintainer
|
||||
public Task<AdpToken?> GetAdpTokenAsync()
|
||||
=> AdpToken is null ? Task.FromResult((AdpToken?)null) : Task.FromResult((AdpToken?)new AdpToken(AdpToken));
|
||||
|
||||
public Task<PrivateKey?> GetPrivateKeyAsync()
|
||||
public Task<PrivateKey?> GetPrivateKeyAsync()
|
||||
=> DevicePrivateKey is null ? Task.FromResult((PrivateKey?)null) : Task.FromResult((PrivateKey?)new PrivateKey(DevicePrivateKey));
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ public partial class Mkb79Auth
|
||||
AccessTokenExpires = account.IdentityTokens?.ExistingAccessToken.Expires ?? default,
|
||||
LocaleCode = account.Locale?.CountryCode,
|
||||
WithUsername = account.Locale?.WithUsername ?? false,
|
||||
RefreshToken = account.IdentityTokens?.RefreshToken?.Value,
|
||||
RefreshToken = account.IdentityTokens?.RefreshToken?.Value,
|
||||
StoreAuthenticationCookie = account.IdentityTokens?.StoreAuthenticationCookie,
|
||||
WebsiteCookies = new(account.IdentityTokens?.Cookies ?? []),
|
||||
};
|
||||
@@ -196,7 +196,7 @@ public partial class Mkb79Auth
|
||||
|
||||
public static class Serialize
|
||||
{
|
||||
public static string ToJson(this Mkb79Auth self)
|
||||
public static string ToJson(this Mkb79Auth self)
|
||||
=> JObject.Parse(JsonConvert.SerializeObject(self, Converter.Settings)).ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using AudibleApi;
|
||||
using AudibleApi.Cryptography;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http;
|
||||
using AudibleApi.Cryptography;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Dinah.Core.Net.Http;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace AudibleUtilities.Widevine;
|
||||
|
||||
public partial class Cdm
|
||||
@@ -112,7 +111,7 @@ public partial class Cdm
|
||||
var uris = urlArray.Select(u => u.Value<string>()).OfType<string>().Select(u => new Uri(u)).ToArray();
|
||||
|
||||
if (uris.Length == 0)
|
||||
throw new System.IO.InvalidDataException("No CDM url found in JSON: " + fileContents);
|
||||
throw new System.IO.InvalidDataException("No CDM url found in JSON: " + fileContents);
|
||||
|
||||
return uris;
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ public partial class Cdm
|
||||
id = id.Append(new byte[16 - id.Length]);
|
||||
}
|
||||
|
||||
keys[i] = new WidevineKey(new Guid(id,bigEndian: true), keyContainer.Type, keyBytes);
|
||||
keys[i] = new WidevineKey(new Guid(id, bigEndian: true), keyContainer.Type, keyBytes);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@@ -65,11 +65,11 @@ internal class Device
|
||||
public byte[] DecryptSessionKey(byte[] sessionKey)
|
||||
=> CdmKey.Decrypt(sessionKey, RSAEncryptionPadding.OaepSHA1);
|
||||
|
||||
/// <summary>
|
||||
/// Completely managed implementation of RSASSA-PSS using SHA-1.
|
||||
/// https://github.com/bcgit/bc-csharp/blob/master/crypto/src/crypto/signers/PssSigner.cs
|
||||
///
|
||||
/// Absolutely nobody anywhere should use this RSASSA-PSS implementation in anything where they care about security at all. We completely skipped the random salt part of it because libation doesn't need security; it only needs to satisfy Audible server's challenge-response requirements.
|
||||
/// <summary>
|
||||
/// Completely managed implementation of RSASSA-PSS using SHA-1.
|
||||
/// https://github.com/bcgit/bc-csharp/blob/master/crypto/src/crypto/signers/PssSigner.cs
|
||||
///
|
||||
/// Absolutely nobody anywhere should use this RSASSA-PSS implementation in anything where they care about security at all. We completely skipped the random salt part of it because libation doesn't need security; it only needs to satisfy Audible server's challenge-response requirements.
|
||||
/// </summary>
|
||||
private static class PssSha1Signer
|
||||
{
|
||||
|
||||
@@ -1,372 +1,369 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using System;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Postgres.Migrations;
|
||||
|
||||
namespace DataLayer.Postgres.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class InitialPostgres : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialPostgres : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Books",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
AudibleProductId = table.Column<string>(type: "text", nullable: true),
|
||||
Title = table.Column<string>(type: "text", nullable: true),
|
||||
Subtitle = table.Column<string>(type: "text", nullable: true),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
LengthInMinutes = table.Column<int>(type: "integer", nullable: false),
|
||||
ContentType = table.Column<int>(type: "integer", nullable: false),
|
||||
Locale = table.Column<string>(type: "text", nullable: true),
|
||||
PictureId = table.Column<string>(type: "text", nullable: true),
|
||||
PictureLarge = table.Column<string>(type: "text", nullable: true),
|
||||
IsAbridged = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsSpatial = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DatePublished = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
Language = table.Column<string>(type: "text", nullable: true),
|
||||
Rating_OverallRating = table.Column<float>(type: "real", nullable: true),
|
||||
Rating_PerformanceRating = table.Column<float>(type: "real", nullable: true),
|
||||
Rating_StoryRating = table.Column<float>(type: "real", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Books", x => x.BookId);
|
||||
});
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Books",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
AudibleProductId = table.Column<string>(type: "text", nullable: true),
|
||||
Title = table.Column<string>(type: "text", nullable: true),
|
||||
Subtitle = table.Column<string>(type: "text", nullable: true),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
LengthInMinutes = table.Column<int>(type: "integer", nullable: false),
|
||||
ContentType = table.Column<int>(type: "integer", nullable: false),
|
||||
Locale = table.Column<string>(type: "text", nullable: true),
|
||||
PictureId = table.Column<string>(type: "text", nullable: true),
|
||||
PictureLarge = table.Column<string>(type: "text", nullable: true),
|
||||
IsAbridged = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsSpatial = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DatePublished = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
Language = table.Column<string>(type: "text", nullable: true),
|
||||
Rating_OverallRating = table.Column<float>(type: "real", nullable: true),
|
||||
Rating_PerformanceRating = table.Column<float>(type: "real", nullable: true),
|
||||
Rating_StoryRating = table.Column<float>(type: "real", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Books", x => x.BookId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
CategoryId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
AudibleCategoryId = table.Column<string>(type: "text", nullable: true),
|
||||
Name = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.CategoryId);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
CategoryId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
AudibleCategoryId = table.Column<string>(type: "text", nullable: true),
|
||||
Name = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.CategoryId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CategoryLadders",
|
||||
columns: table => new
|
||||
{
|
||||
CategoryLadderId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CategoryLadders", x => x.CategoryLadderId);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CategoryLadders",
|
||||
columns: table => new
|
||||
{
|
||||
CategoryLadderId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CategoryLadders", x => x.CategoryLadderId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Contributors",
|
||||
columns: table => new
|
||||
{
|
||||
ContributorId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
AudibleContributorId = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Contributors", x => x.ContributorId);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Contributors",
|
||||
columns: table => new
|
||||
{
|
||||
ContributorId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
AudibleContributorId = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Contributors", x => x.ContributorId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Series",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
AudibleSeriesId = table.Column<string>(type: "text", nullable: true),
|
||||
Name = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Series", x => x.SeriesId);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Series",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
AudibleSeriesId = table.Column<string>(type: "text", nullable: true),
|
||||
Name = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Series", x => x.SeriesId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LibraryBooks",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
DateAdded = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
|
||||
Account = table.Column<string>(type: "text", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
AbsentFromLastScan = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IncludedUntil = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LibraryBooks", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_LibraryBooks_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LibraryBooks",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
DateAdded = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
|
||||
Account = table.Column<string>(type: "text", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
AbsentFromLastScan = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IncludedUntil = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LibraryBooks", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_LibraryBooks_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Supplement",
|
||||
columns: table => new
|
||||
{
|
||||
SupplementId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
Url = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Supplement", x => x.SupplementId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Supplement_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Supplement",
|
||||
columns: table => new
|
||||
{
|
||||
SupplementId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
Url = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Supplement", x => x.SupplementId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Supplement_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserDefinedItem",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
LastDownloaded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
LastDownloadedVersion = table.Column<string>(type: "text", nullable: true),
|
||||
LastDownloadedFormat = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastDownloadedFileVersion = table.Column<string>(type: "text", nullable: true),
|
||||
Tags = table.Column<string>(type: "text", nullable: true),
|
||||
Rating_OverallRating = table.Column<float>(type: "real", nullable: true),
|
||||
Rating_PerformanceRating = table.Column<float>(type: "real", nullable: true),
|
||||
Rating_StoryRating = table.Column<float>(type: "real", nullable: true),
|
||||
BookStatus = table.Column<int>(type: "integer", nullable: false),
|
||||
PdfStatus = table.Column<int>(type: "integer", nullable: true),
|
||||
IsFinished = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserDefinedItem", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserDefinedItem_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserDefinedItem",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
LastDownloaded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
LastDownloadedVersion = table.Column<string>(type: "text", nullable: true),
|
||||
LastDownloadedFormat = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastDownloadedFileVersion = table.Column<string>(type: "text", nullable: true),
|
||||
Tags = table.Column<string>(type: "text", nullable: true),
|
||||
Rating_OverallRating = table.Column<float>(type: "real", nullable: true),
|
||||
Rating_PerformanceRating = table.Column<float>(type: "real", nullable: true),
|
||||
Rating_StoryRating = table.Column<float>(type: "real", nullable: true),
|
||||
BookStatus = table.Column<int>(type: "integer", nullable: false),
|
||||
PdfStatus = table.Column<int>(type: "integer", nullable: true),
|
||||
IsFinished = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserDefinedItem", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserDefinedItem_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookCategory",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
CategoryLadderId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookCategory", x => new { x.BookId, x.CategoryLadderId });
|
||||
table.ForeignKey(
|
||||
name: "FK_BookCategory_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookCategory_CategoryLadders_CategoryLadderId",
|
||||
column: x => x.CategoryLadderId,
|
||||
principalTable: "CategoryLadders",
|
||||
principalColumn: "CategoryLadderId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookCategory",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
CategoryLadderId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookCategory", x => new { x.BookId, x.CategoryLadderId });
|
||||
table.ForeignKey(
|
||||
name: "FK_BookCategory_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookCategory_CategoryLadders_CategoryLadderId",
|
||||
column: x => x.CategoryLadderId,
|
||||
principalTable: "CategoryLadders",
|
||||
principalColumn: "CategoryLadderId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CategoryCategoryLadder",
|
||||
columns: table => new
|
||||
{
|
||||
_categoriesCategoryId = table.Column<int>(type: "integer", nullable: false),
|
||||
_categoryLaddersCategoryLadderId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CategoryCategoryLadder", x => new { x._categoriesCategoryId, x._categoryLaddersCategoryLadderId });
|
||||
table.ForeignKey(
|
||||
name: "FK_CategoryCategoryLadder_Categories__categoriesCategoryId",
|
||||
column: x => x._categoriesCategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_CategoryCategoryLadder_CategoryLadders__categoryLaddersCate~",
|
||||
column: x => x._categoryLaddersCategoryLadderId,
|
||||
principalTable: "CategoryLadders",
|
||||
principalColumn: "CategoryLadderId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CategoryCategoryLadder",
|
||||
columns: table => new
|
||||
{
|
||||
_categoriesCategoryId = table.Column<int>(type: "integer", nullable: false),
|
||||
_categoryLaddersCategoryLadderId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CategoryCategoryLadder", x => new { x._categoriesCategoryId, x._categoryLaddersCategoryLadderId });
|
||||
table.ForeignKey(
|
||||
name: "FK_CategoryCategoryLadder_Categories__categoriesCategoryId",
|
||||
column: x => x._categoriesCategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_CategoryCategoryLadder_CategoryLadders__categoryLaddersCate~",
|
||||
column: x => x._categoryLaddersCategoryLadderId,
|
||||
principalTable: "CategoryLadders",
|
||||
principalColumn: "CategoryLadderId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookContributor",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
ContributorId = table.Column<int>(type: "integer", nullable: false),
|
||||
Role = table.Column<int>(type: "integer", nullable: false),
|
||||
Order = table.Column<byte>(type: "smallint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookContributor", x => new { x.BookId, x.ContributorId, x.Role });
|
||||
table.ForeignKey(
|
||||
name: "FK_BookContributor_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookContributor_Contributors_ContributorId",
|
||||
column: x => x.ContributorId,
|
||||
principalTable: "Contributors",
|
||||
principalColumn: "ContributorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookContributor",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
ContributorId = table.Column<int>(type: "integer", nullable: false),
|
||||
Role = table.Column<int>(type: "integer", nullable: false),
|
||||
Order = table.Column<byte>(type: "smallint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookContributor", x => new { x.BookId, x.ContributorId, x.Role });
|
||||
table.ForeignKey(
|
||||
name: "FK_BookContributor_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookContributor_Contributors_ContributorId",
|
||||
column: x => x.ContributorId,
|
||||
principalTable: "Contributors",
|
||||
principalColumn: "ContributorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesBook",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesId = table.Column<int>(type: "integer", nullable: false),
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
Order = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesBook", x => new { x.SeriesId, x.BookId });
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBook_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBook_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "SeriesId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesBook",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesId = table.Column<int>(type: "integer", nullable: false),
|
||||
BookId = table.Column<int>(type: "integer", nullable: false),
|
||||
Order = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesBook", x => new { x.SeriesId, x.BookId });
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBook_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBook_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "SeriesId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Contributors",
|
||||
columns: new[] { "ContributorId", "AudibleContributorId", "Name" },
|
||||
values: new object[] { -1, null, "" });
|
||||
migrationBuilder.InsertData(
|
||||
table: "Contributors",
|
||||
columns: new[] { "ContributorId", "AudibleContributorId", "Name" },
|
||||
values: new object[] { -1, null, "" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookCategory_BookId",
|
||||
table: "BookCategory",
|
||||
column: "BookId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookCategory_BookId",
|
||||
table: "BookCategory",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookCategory_CategoryLadderId",
|
||||
table: "BookCategory",
|
||||
column: "CategoryLadderId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookCategory_CategoryLadderId",
|
||||
table: "BookCategory",
|
||||
column: "CategoryLadderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookContributor_BookId",
|
||||
table: "BookContributor",
|
||||
column: "BookId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookContributor_BookId",
|
||||
table: "BookContributor",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookContributor_ContributorId",
|
||||
table: "BookContributor",
|
||||
column: "ContributorId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookContributor_ContributorId",
|
||||
table: "BookContributor",
|
||||
column: "ContributorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_AudibleProductId",
|
||||
table: "Books",
|
||||
column: "AudibleProductId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_AudibleProductId",
|
||||
table: "Books",
|
||||
column: "AudibleProductId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_AudibleCategoryId",
|
||||
table: "Categories",
|
||||
column: "AudibleCategoryId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_AudibleCategoryId",
|
||||
table: "Categories",
|
||||
column: "AudibleCategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CategoryCategoryLadder__categoryLaddersCategoryLadderId",
|
||||
table: "CategoryCategoryLadder",
|
||||
column: "_categoryLaddersCategoryLadderId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CategoryCategoryLadder__categoryLaddersCategoryLadderId",
|
||||
table: "CategoryCategoryLadder",
|
||||
column: "_categoryLaddersCategoryLadderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Contributors_Name",
|
||||
table: "Contributors",
|
||||
column: "Name");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Contributors_Name",
|
||||
table: "Contributors",
|
||||
column: "Name");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Series_AudibleSeriesId",
|
||||
table: "Series",
|
||||
column: "AudibleSeriesId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Series_AudibleSeriesId",
|
||||
table: "Series",
|
||||
column: "AudibleSeriesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBook_BookId",
|
||||
table: "SeriesBook",
|
||||
column: "BookId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBook_BookId",
|
||||
table: "SeriesBook",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBook_SeriesId",
|
||||
table: "SeriesBook",
|
||||
column: "SeriesId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBook_SeriesId",
|
||||
table: "SeriesBook",
|
||||
column: "SeriesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Supplement_BookId",
|
||||
table: "Supplement",
|
||||
column: "BookId");
|
||||
}
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Supplement_BookId",
|
||||
table: "Supplement",
|
||||
column: "BookId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookCategory");
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookCategory");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookContributor");
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookContributor");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CategoryCategoryLadder");
|
||||
migrationBuilder.DropTable(
|
||||
name: "CategoryCategoryLadder");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LibraryBooks");
|
||||
migrationBuilder.DropTable(
|
||||
name: "LibraryBooks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesBook");
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesBook");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Supplement");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Supplement");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserDefinedItem");
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Contributors");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Contributors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CategoryLadders");
|
||||
migrationBuilder.DropTable(
|
||||
name: "CategoryLadders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Series");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Series");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Books");
|
||||
}
|
||||
}
|
||||
migrationBuilder.DropTable(
|
||||
name: "Books");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,98 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Postgres.Migrations;
|
||||
|
||||
namespace DataLayer.Postgres.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsAudiblePlus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsAudiblePlus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks");
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
}
|
||||
}
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,262 +1,259 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Postgres.Migrations;
|
||||
|
||||
namespace DataLayer.Postgres.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class MakeDbNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakeDbNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Name",
|
||||
table: "Categories");
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Name",
|
||||
table: "Categories");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Url",
|
||||
table: "Supplement",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Url",
|
||||
table: "Supplement",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleSeriesId",
|
||||
table: "Series",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleSeriesId",
|
||||
table: "Series",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Account",
|
||||
table: "LibraryBooks",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Account",
|
||||
table: "LibraryBooks",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Contributors",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Contributors",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleCategoryId",
|
||||
table: "Categories",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleCategoryId",
|
||||
table: "Categories",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Title",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Title",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleProductId",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
}
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleProductId",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Url",
|
||||
table: "Supplement",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Url",
|
||||
table: "Supplement",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleSeriesId",
|
||||
table: "Series",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleSeriesId",
|
||||
table: "Series",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Account",
|
||||
table: "LibraryBooks",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Account",
|
||||
table: "LibraryBooks",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Contributors",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Contributors",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleCategoryId",
|
||||
table: "Categories",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleCategoryId",
|
||||
table: "Categories",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Name",
|
||||
table: "Categories",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Name",
|
||||
table: "Categories",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Title",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Title",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "Books",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleProductId",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
}
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleProductId",
|
||||
table: "Books",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace DataLayer.Postgres
|
||||
namespace DataLayer.Postgres;
|
||||
|
||||
public class PostgresContextFactory : IDesignTimeDbContextFactory<LibationContext>
|
||||
{
|
||||
public class PostgresContextFactory : IDesignTimeDbContextFactory<LibationContext>
|
||||
{
|
||||
public LibationContext CreateDbContext(string[] args)
|
||||
{
|
||||
return LibationContextFactory.CreatePostgres(string.Empty);
|
||||
}
|
||||
}
|
||||
public LibationContext CreateDbContext(string[] args)
|
||||
{
|
||||
return LibationContextFactory.CreatePostgres(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,294 +1,293 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
public partial class Fresh : Migration
|
||||
{
|
||||
public partial class Fresh : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
CategoryId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AudibleCategoryId = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
ParentCategoryCategoryId = table.Column<int>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.CategoryId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Categories_Categories_ParentCategoryCategoryId",
|
||||
column: x => x.ParentCategoryCategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
CategoryId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AudibleCategoryId = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
ParentCategoryCategoryId = table.Column<int>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.CategoryId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Categories_Categories_ParentCategoryCategoryId",
|
||||
column: x => x.ParentCategoryCategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Contributors",
|
||||
columns: table => new
|
||||
{
|
||||
ContributorId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
AudibleContributorId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Contributors", x => x.ContributorId);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Contributors",
|
||||
columns: table => new
|
||||
{
|
||||
ContributorId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
AudibleContributorId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Contributors", x => x.ContributorId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Series",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AudibleSeriesId = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Series", x => x.SeriesId);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Series",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AudibleSeriesId = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Series", x => x.SeriesId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Books",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AudibleProductId = table.Column<string>(nullable: true),
|
||||
Title = table.Column<string>(nullable: true),
|
||||
Description = table.Column<string>(nullable: true),
|
||||
LengthInMinutes = table.Column<int>(nullable: false),
|
||||
PictureId = table.Column<string>(nullable: true),
|
||||
IsAbridged = table.Column<bool>(nullable: false),
|
||||
DatePublished = table.Column<DateTime>(nullable: true),
|
||||
CategoryId = table.Column<int>(nullable: false),
|
||||
Rating_OverallRating = table.Column<float>(nullable: true),
|
||||
Rating_PerformanceRating = table.Column<float>(nullable: true),
|
||||
Rating_StoryRating = table.Column<float>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Books", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Books_Categories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Books",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AudibleProductId = table.Column<string>(nullable: true),
|
||||
Title = table.Column<string>(nullable: true),
|
||||
Description = table.Column<string>(nullable: true),
|
||||
LengthInMinutes = table.Column<int>(nullable: false),
|
||||
PictureId = table.Column<string>(nullable: true),
|
||||
IsAbridged = table.Column<bool>(nullable: false),
|
||||
DatePublished = table.Column<DateTime>(nullable: true),
|
||||
CategoryId = table.Column<int>(nullable: false),
|
||||
Rating_OverallRating = table.Column<float>(nullable: true),
|
||||
Rating_PerformanceRating = table.Column<float>(nullable: true),
|
||||
Rating_StoryRating = table.Column<float>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Books", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Books_Categories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookContributor",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
ContributorId = table.Column<int>(nullable: false),
|
||||
Role = table.Column<int>(nullable: false),
|
||||
Order = table.Column<byte>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookContributor", x => new { x.BookId, x.ContributorId, x.Role });
|
||||
table.ForeignKey(
|
||||
name: "FK_BookContributor_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookContributor_Contributors_ContributorId",
|
||||
column: x => x.ContributorId,
|
||||
principalTable: "Contributors",
|
||||
principalColumn: "ContributorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookContributor",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
ContributorId = table.Column<int>(nullable: false),
|
||||
Role = table.Column<int>(nullable: false),
|
||||
Order = table.Column<byte>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookContributor", x => new { x.BookId, x.ContributorId, x.Role });
|
||||
table.ForeignKey(
|
||||
name: "FK_BookContributor_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookContributor_Contributors_ContributorId",
|
||||
column: x => x.ContributorId,
|
||||
principalTable: "Contributors",
|
||||
principalColumn: "ContributorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Library",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
DateAdded = table.Column<DateTime>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Library", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Library_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Library",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
DateAdded = table.Column<DateTime>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Library", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Library_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesBook",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesId = table.Column<int>(nullable: false),
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
Index = table.Column<float>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesBook", x => new { x.SeriesId, x.BookId });
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBook_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBook_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "SeriesId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesBook",
|
||||
columns: table => new
|
||||
{
|
||||
SeriesId = table.Column<int>(nullable: false),
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
Index = table.Column<float>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesBook", x => new { x.SeriesId, x.BookId });
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBook_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBook_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "SeriesId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Supplement",
|
||||
columns: table => new
|
||||
{
|
||||
SupplementId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
Url = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Supplement", x => x.SupplementId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Supplement_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Supplement",
|
||||
columns: table => new
|
||||
{
|
||||
SupplementId = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
Url = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Supplement", x => x.SupplementId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Supplement_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserDefinedItem",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
Tags = table.Column<string>(nullable: true),
|
||||
Rating_OverallRating = table.Column<float>(nullable: true),
|
||||
Rating_PerformanceRating = table.Column<float>(nullable: true),
|
||||
Rating_StoryRating = table.Column<float>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserDefinedItem", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserDefinedItem_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserDefinedItem",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
Tags = table.Column<string>(nullable: true),
|
||||
Rating_OverallRating = table.Column<float>(nullable: true),
|
||||
Rating_PerformanceRating = table.Column<float>(nullable: true),
|
||||
Rating_StoryRating = table.Column<float>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserDefinedItem", x => x.BookId);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserDefinedItem_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Categories",
|
||||
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
|
||||
values: new object[] { -1, "", "", null });
|
||||
migrationBuilder.InsertData(
|
||||
table: "Categories",
|
||||
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
|
||||
values: new object[] { -1, "", "", null });
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Contributors",
|
||||
columns: new[] { "ContributorId", "AudibleContributorId", "Name" },
|
||||
values: new object[] { -1, null, "" });
|
||||
migrationBuilder.InsertData(
|
||||
table: "Contributors",
|
||||
columns: new[] { "ContributorId", "AudibleContributorId", "Name" },
|
||||
values: new object[] { -1, null, "" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookContributor_BookId",
|
||||
table: "BookContributor",
|
||||
column: "BookId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookContributor_BookId",
|
||||
table: "BookContributor",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookContributor_ContributorId",
|
||||
table: "BookContributor",
|
||||
column: "ContributorId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookContributor_ContributorId",
|
||||
table: "BookContributor",
|
||||
column: "ContributorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_AudibleProductId",
|
||||
table: "Books",
|
||||
column: "AudibleProductId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_AudibleProductId",
|
||||
table: "Books",
|
||||
column: "AudibleProductId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_CategoryId",
|
||||
table: "Books",
|
||||
column: "CategoryId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_CategoryId",
|
||||
table: "Books",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_AudibleCategoryId",
|
||||
table: "Categories",
|
||||
column: "AudibleCategoryId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_AudibleCategoryId",
|
||||
table: "Categories",
|
||||
column: "AudibleCategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories",
|
||||
column: "ParentCategoryCategoryId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories",
|
||||
column: "ParentCategoryCategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Contributors_Name",
|
||||
table: "Contributors",
|
||||
column: "Name");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Contributors_Name",
|
||||
table: "Contributors",
|
||||
column: "Name");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Series_AudibleSeriesId",
|
||||
table: "Series",
|
||||
column: "AudibleSeriesId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Series_AudibleSeriesId",
|
||||
table: "Series",
|
||||
column: "AudibleSeriesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBook_BookId",
|
||||
table: "SeriesBook",
|
||||
column: "BookId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBook_BookId",
|
||||
table: "SeriesBook",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBook_SeriesId",
|
||||
table: "SeriesBook",
|
||||
column: "SeriesId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBook_SeriesId",
|
||||
table: "SeriesBook",
|
||||
column: "SeriesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Supplement_BookId",
|
||||
table: "Supplement",
|
||||
column: "BookId");
|
||||
}
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Supplement_BookId",
|
||||
table: "Supplement",
|
||||
column: "BookId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookContributor");
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookContributor");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Library");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesBook");
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesBook");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Supplement");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Supplement");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserDefinedItem");
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Contributors");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Contributors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Series");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Series");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Books");
|
||||
migrationBuilder.DropTable(
|
||||
name: "Books");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
}
|
||||
}
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
public partial class AddLocaleAndAccount : Migration
|
||||
{
|
||||
public partial class AddLocaleAndAccount : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Account",
|
||||
table: "Library",
|
||||
nullable: true);
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Account",
|
||||
table: "Library",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
nullable: true);
|
||||
}
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Account",
|
||||
table: "Library");
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Account",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Locale",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Locale",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
public partial class AddAaxcDecryptionKeys : Migration
|
||||
{
|
||||
public partial class AddAaxcDecryptionKeys : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleIV",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleIV",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleKey",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleKey",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleIV",
|
||||
table: "Books");
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleIV",
|
||||
table: "Books");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleKey",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleKey",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
public partial class RemoveAaxcDecryptionKeys : Migration
|
||||
{
|
||||
public partial class RemoveAaxcDecryptionKeys : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleIV",
|
||||
table: "Books");
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleIV",
|
||||
table: "Books");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleKey",
|
||||
table: "Books");
|
||||
}
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleKey",
|
||||
table: "Books");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleIV",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleIV",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleKey",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleKey",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
public partial class AddLiberatedStatus : Migration
|
||||
{
|
||||
public partial class AddLiberatedStatus : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "BookStatus",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "BookStatus",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PdfStatus",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PdfStatus",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem");
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookStatus",
|
||||
table: "UserDefinedItem");
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookStatus",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PdfStatus",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
}
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PdfStatus",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class RemoveUdiBookLocation : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
public partial class RemoveUdiBookLocation : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class BookIsEpisode : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ContentType",
|
||||
table: "Books",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ContentType",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
public partial class BookIsEpisode : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ContentType",
|
||||
table: "Books",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ContentType",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,62 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
public partial class RenameLibraryBooks : Migration
|
||||
{
|
||||
public partial class RenameLibraryBooks : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Library_Books_BookId",
|
||||
table: "Library");
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Library_Books_BookId",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Library",
|
||||
table: "Library");
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Library",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Library",
|
||||
newName: "LibraryBooks");
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Library",
|
||||
newName: "LibraryBooks");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_LibraryBooks",
|
||||
table: "LibraryBooks",
|
||||
column: "BookId");
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_LibraryBooks",
|
||||
table: "LibraryBooks",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_LibraryBooks_Books_BookId",
|
||||
table: "LibraryBooks",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_LibraryBooks_Books_BookId",
|
||||
table: "LibraryBooks",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_LibraryBooks_Books_BookId",
|
||||
table: "LibraryBooks");
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_LibraryBooks_Books_BookId",
|
||||
table: "LibraryBooks");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_LibraryBooks",
|
||||
table: "LibraryBooks");
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_LibraryBooks",
|
||||
table: "LibraryBooks");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "LibraryBooks",
|
||||
newName: "Library");
|
||||
migrationBuilder.RenameTable(
|
||||
name: "LibraryBooks",
|
||||
newName: "Library");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Library",
|
||||
table: "Library",
|
||||
column: "BookId");
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Library",
|
||||
table: "Library",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Library_Books_BookId",
|
||||
table: "Library",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Library_Books_BookId",
|
||||
table: "Library",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
public partial class AddSeriesOrderString : Migration
|
||||
{
|
||||
public partial class AddSeriesOrderString : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Index",
|
||||
table: "SeriesBook");
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Index",
|
||||
table: "SeriesBook");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Order",
|
||||
table: "SeriesBook",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Order",
|
||||
table: "SeriesBook",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Order",
|
||||
table: "SeriesBook");
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Order",
|
||||
table: "SeriesBook");
|
||||
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "Index",
|
||||
table: "SeriesBook",
|
||||
type: "REAL",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "Index",
|
||||
table: "SeriesBook",
|
||||
type: "REAL",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
public partial class AddPictureIDLargeMigration : Migration
|
||||
{
|
||||
public partial class AddPictureIDLargeMigration : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PictureLarge",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PictureLarge",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PictureLarge",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PictureLarge",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
public partial class AddAudioFormat : Migration
|
||||
{
|
||||
public partial class AddAudioFormat : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "_audioFormat",
|
||||
table: "Books",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "_audioFormat",
|
||||
table: "Books",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "_audioFormat",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "_audioFormat",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class LibraryBookIsDeleted : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class LibraryBookIsDeleted : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "LibraryBooks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "LibraryBooks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "LibraryBooks");
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "LibraryBooks");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddBookLanguage : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBookLanguage : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Language",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Language",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Language",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Language",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,36 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddLastDownloadedInfo : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddLastDownloadedInfo : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastDownloaded",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastDownloaded",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LastDownloadedVersion",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LastDownloadedVersion",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloaded",
|
||||
table: "UserDefinedItem");
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloaded",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloadedVersion",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
}
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloadedVersion",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddAbsentFromLastScan : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAbsentFromLastScan : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AbsentFromLastScan",
|
||||
table: "LibraryBooks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AbsentFromLastScan",
|
||||
table: "LibraryBooks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AbsentFromLastScan",
|
||||
table: "LibraryBooks");
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AbsentFromLastScan",
|
||||
table: "LibraryBooks");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddBookSubtitle : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBookSubtitle : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Subtitle",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Subtitle",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,174 +1,171 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddCategoryLadder : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCategoryLadder : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Books_Categories_CategoryId",
|
||||
table: "Books");
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Books_Categories_CategoryId",
|
||||
table: "Books");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Categories_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories");
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Categories_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories");
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Books_CategoryId",
|
||||
table: "Books");
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Books_CategoryId",
|
||||
table: "Books");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Categories",
|
||||
keyColumn: "CategoryId",
|
||||
keyValue: -1);
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Categories",
|
||||
keyColumn: "CategoryId",
|
||||
keyValue: -1);
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ParentCategoryCategoryId",
|
||||
table: "Categories");
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ParentCategoryCategoryId",
|
||||
table: "Categories");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CategoryId",
|
||||
table: "Books");
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CategoryId",
|
||||
table: "Books");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CategoryLadders",
|
||||
columns: table => new
|
||||
{
|
||||
CategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CategoryLadders", x => x.CategoryLadderId);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CategoryLadders",
|
||||
columns: table => new
|
||||
{
|
||||
CategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CategoryLadders", x => x.CategoryLadderId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookCategory",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookCategory", x => new { x.BookId, x.CategoryLadderId });
|
||||
table.ForeignKey(
|
||||
name: "FK_BookCategory_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookCategory_CategoryLadders_CategoryLadderId",
|
||||
column: x => x.CategoryLadderId,
|
||||
principalTable: "CategoryLadders",
|
||||
principalColumn: "CategoryLadderId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookCategory",
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookCategory", x => new { x.BookId, x.CategoryLadderId });
|
||||
table.ForeignKey(
|
||||
name: "FK_BookCategory_Books_BookId",
|
||||
column: x => x.BookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookCategory_CategoryLadders_CategoryLadderId",
|
||||
column: x => x.CategoryLadderId,
|
||||
principalTable: "CategoryLadders",
|
||||
principalColumn: "CategoryLadderId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CategoryCategoryLadder",
|
||||
columns: table => new
|
||||
{
|
||||
_categoriesCategoryId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
_categoryLaddersCategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CategoryCategoryLadder", x => new { x._categoriesCategoryId, x._categoryLaddersCategoryLadderId });
|
||||
table.ForeignKey(
|
||||
name: "FK_CategoryCategoryLadder_Categories__categoriesCategoryId",
|
||||
column: x => x._categoriesCategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_CategoryCategoryLadder_CategoryLadders__categoryLaddersCategoryLadderId",
|
||||
column: x => x._categoryLaddersCategoryLadderId,
|
||||
principalTable: "CategoryLadders",
|
||||
principalColumn: "CategoryLadderId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CategoryCategoryLadder",
|
||||
columns: table => new
|
||||
{
|
||||
_categoriesCategoryId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
_categoryLaddersCategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CategoryCategoryLadder", x => new { x._categoriesCategoryId, x._categoryLaddersCategoryLadderId });
|
||||
table.ForeignKey(
|
||||
name: "FK_CategoryCategoryLadder_Categories__categoriesCategoryId",
|
||||
column: x => x._categoriesCategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_CategoryCategoryLadder_CategoryLadders__categoryLaddersCategoryLadderId",
|
||||
column: x => x._categoryLaddersCategoryLadderId,
|
||||
principalTable: "CategoryLadders",
|
||||
principalColumn: "CategoryLadderId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookCategory_BookId",
|
||||
table: "BookCategory",
|
||||
column: "BookId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookCategory_BookId",
|
||||
table: "BookCategory",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookCategory_CategoryLadderId",
|
||||
table: "BookCategory",
|
||||
column: "CategoryLadderId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookCategory_CategoryLadderId",
|
||||
table: "BookCategory",
|
||||
column: "CategoryLadderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CategoryCategoryLadder__categoryLaddersCategoryLadderId",
|
||||
table: "CategoryCategoryLadder",
|
||||
column: "_categoryLaddersCategoryLadderId");
|
||||
}
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CategoryCategoryLadder__categoryLaddersCategoryLadderId",
|
||||
table: "CategoryCategoryLadder",
|
||||
column: "_categoryLaddersCategoryLadderId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookCategory");
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookCategory");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CategoryCategoryLadder");
|
||||
migrationBuilder.DropTable(
|
||||
name: "CategoryCategoryLadder");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CategoryLadders");
|
||||
migrationBuilder.DropTable(
|
||||
name: "CategoryLadders");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ParentCategoryCategoryId",
|
||||
table: "Categories",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ParentCategoryCategoryId",
|
||||
table: "Categories",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CategoryId",
|
||||
table: "Books",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CategoryId",
|
||||
table: "Books",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Categories",
|
||||
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
|
||||
values: new object[] { -1, "", "", null });
|
||||
migrationBuilder.InsertData(
|
||||
table: "Categories",
|
||||
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
|
||||
values: new object[] { -1, "", "", null });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories",
|
||||
column: "ParentCategoryCategoryId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories",
|
||||
column: "ParentCategoryCategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_CategoryId",
|
||||
table: "Books",
|
||||
column: "CategoryId");
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_CategoryId",
|
||||
table: "Books",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Books_Categories_CategoryId",
|
||||
table: "Books",
|
||||
column: "CategoryId",
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Books_Categories_CategoryId",
|
||||
table: "Books",
|
||||
column: "CategoryId",
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Categories_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories",
|
||||
column: "ParentCategoryCategoryId",
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId");
|
||||
}
|
||||
}
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Categories_Categories_ParentCategoryCategoryId",
|
||||
table: "Categories",
|
||||
column: "ParentCategoryCategoryId",
|
||||
principalTable: "Categories",
|
||||
principalColumn: "CategoryId");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class MyComment : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MyComment : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsFinished",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsFinished",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsFinished",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsFinished",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,45 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddAudioFormatData : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAudioFormatData : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "_audioFormat",
|
||||
table: "Books",
|
||||
newName: "IsSpatial");
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "_audioFormat",
|
||||
table: "Books",
|
||||
newName: "IsSpatial");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LastDownloadedFileVersion",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LastDownloadedFileVersion",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "LastDownloadedFormat",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "LastDownloadedFormat",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloadedFileVersion",
|
||||
table: "UserDefinedItem");
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloadedFileVersion",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloadedFormat",
|
||||
table: "UserDefinedItem");
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloadedFormat",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "IsSpatial",
|
||||
table: "Books",
|
||||
newName: "_audioFormat");
|
||||
}
|
||||
}
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "IsSpatial",
|
||||
table: "Books",
|
||||
newName: "_audioFormat");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddIncludedUntil : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIncludedUntil : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "IncludedUntil",
|
||||
table: "LibraryBooks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "IncludedUntil",
|
||||
table: "LibraryBooks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IncludedUntil",
|
||||
table: "LibraryBooks");
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IncludedUntil",
|
||||
table: "LibraryBooks");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,98 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsAudiblePlus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsAudiblePlus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks");
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
}
|
||||
}
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,262 +1,259 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
namespace DataLayer.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
/// <inheritdoc />
|
||||
public partial class MakeDbNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakeDbNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Name",
|
||||
table: "Categories");
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Name",
|
||||
table: "Categories");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Url",
|
||||
table: "Supplement",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Url",
|
||||
table: "Supplement",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleSeriesId",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleSeriesId",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Account",
|
||||
table: "LibraryBooks",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Account",
|
||||
table: "LibraryBooks",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Contributors",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Contributors",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleCategoryId",
|
||||
table: "Categories",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleCategoryId",
|
||||
table: "Categories",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Title",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Title",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleProductId",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
}
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleProductId",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Url",
|
||||
table: "Supplement",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Url",
|
||||
table: "Supplement",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleSeriesId",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleSeriesId",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Account",
|
||||
table: "LibraryBooks",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Account",
|
||||
table: "LibraryBooks",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Contributors",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Contributors",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleCategoryId",
|
||||
table: "Categories",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleCategoryId",
|
||||
table: "Categories",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Name",
|
||||
table: "Categories",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Name",
|
||||
table: "Categories",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Title",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Title",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "Books",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Locale",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleProductId",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
}
|
||||
}
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AudibleProductId",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace DataLayer.Sqlite
|
||||
namespace DataLayer.Sqlite;
|
||||
|
||||
public class SqliteContextFactory : IDesignTimeDbContextFactory<LibationContext>
|
||||
{
|
||||
public class SqliteContextFactory : IDesignTimeDbContextFactory<LibationContext>
|
||||
{
|
||||
public LibationContext CreateDbContext(string[] args)
|
||||
{
|
||||
return LibationContextFactory.CreateSqlite(string.Empty);
|
||||
}
|
||||
}
|
||||
public LibationContext CreateDbContext(string[] args)
|
||||
{
|
||||
return LibationContextFactory.CreateSqlite(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
internal class BookCategoryConfig : IEntityTypeConfiguration<BookCategory>
|
||||
{
|
||||
internal class BookCategoryConfig : IEntityTypeConfiguration<BookCategory>
|
||||
public void Configure(EntityTypeBuilder<BookCategory> entity)
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BookCategory> entity)
|
||||
{
|
||||
entity.HasKey(bc => new { bc.BookId, bc.CategoryLadderId });
|
||||
entity.HasKey(bc => new { bc.BookId, bc.CategoryLadderId });
|
||||
|
||||
entity.HasIndex(bc => bc.BookId);
|
||||
entity.HasIndex(bc => bc.CategoryLadderId);
|
||||
entity.HasIndex(bc => bc.BookId);
|
||||
entity.HasIndex(bc => bc.CategoryLadderId);
|
||||
|
||||
entity
|
||||
.HasOne(bc => bc.Book)
|
||||
.WithMany(b => b.CategoriesLink)
|
||||
.HasForeignKey(bc => bc.BookId);
|
||||
entity
|
||||
.HasOne(bc => bc.Book)
|
||||
.WithMany(b => b.CategoriesLink)
|
||||
.HasForeignKey(bc => bc.BookId);
|
||||
|
||||
entity
|
||||
.HasOne(bc => bc.CategoryLadder)
|
||||
.WithMany(c => c.BooksLink)
|
||||
.HasForeignKey(bc => bc.CategoryLadderId);
|
||||
}
|
||||
entity
|
||||
.HasOne(bc => bc.CategoryLadder)
|
||||
.WithMany(c => c.BooksLink)
|
||||
.HasForeignKey(bc => bc.CategoryLadderId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,62 +2,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using System;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
internal class BookConfig : IEntityTypeConfiguration<Book>
|
||||
{
|
||||
internal class BookConfig : IEntityTypeConfiguration<Book>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Book> entity)
|
||||
{
|
||||
entity.HasKey(b => b.BookId);
|
||||
entity.HasIndex(b => b.AudibleProductId);
|
||||
public void Configure(EntityTypeBuilder<Book> entity)
|
||||
{
|
||||
entity.HasKey(b => b.BookId);
|
||||
entity.HasIndex(b => b.AudibleProductId);
|
||||
|
||||
entity.OwnsOne(b => b.Rating);
|
||||
entity.OwnsOne(b => b.Rating);
|
||||
|
||||
//
|
||||
// CRUCIAL: ignore unmapped collections, even get-only
|
||||
//
|
||||
entity.Ignore(nameof(Book.Authors));
|
||||
entity.Ignore(nameof(Book.Narrators));
|
||||
entity.Ignore(nameof(Book.TitleWithSubtitle));
|
||||
entity.Ignore(b => b.Categories);
|
||||
//
|
||||
// CRUCIAL: ignore unmapped collections, even get-only
|
||||
//
|
||||
entity.Ignore(nameof(Book.Authors));
|
||||
entity.Ignore(nameof(Book.Narrators));
|
||||
entity.Ignore(nameof(Book.TitleWithSubtitle));
|
||||
entity.Ignore(b => b.Categories);
|
||||
|
||||
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
|
||||
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
|
||||
entity
|
||||
.OwnsMany(b => b.Supplements, b_s =>
|
||||
{
|
||||
b_s.WithOwner(s => s.Book)
|
||||
.HasForeignKey(s => s.BookId);
|
||||
b_s.HasKey(s => s.SupplementId);
|
||||
});
|
||||
// even though it's owned, we need to map its backing field
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Book.Supplements))
|
||||
?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
|
||||
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
|
||||
entity
|
||||
.OwnsMany(b => b.Supplements, b_s =>
|
||||
{
|
||||
b_s.WithOwner(s => s.Book)
|
||||
.HasForeignKey(s => s.BookId);
|
||||
b_s.HasKey(s => s.SupplementId);
|
||||
});
|
||||
// even though it's owned, we need to map its backing field
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Book.Supplements))
|
||||
?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
// owns it 1:1, store in separate table
|
||||
entity
|
||||
.OwnsOne(b => b.UserDefinedItem, b_udi =>
|
||||
{
|
||||
b_udi.WithOwner(udi => udi.Book)
|
||||
.HasForeignKey(udi => udi.BookId);
|
||||
b_udi.Property(udi => udi.BookId).ValueGeneratedNever();
|
||||
b_udi.ToTable(nameof(Book.UserDefinedItem));
|
||||
// owns it 1:1, store in separate table
|
||||
entity
|
||||
.OwnsOne(b => b.UserDefinedItem, b_udi =>
|
||||
{
|
||||
b_udi.WithOwner(udi => udi.Book)
|
||||
.HasForeignKey(udi => udi.BookId);
|
||||
b_udi.Property(udi => udi.BookId).ValueGeneratedNever();
|
||||
b_udi.ToTable(nameof(Book.UserDefinedItem));
|
||||
|
||||
b_udi.Property(udi => udi.LastDownloaded);
|
||||
b_udi
|
||||
.Property(udi => udi.LastDownloadedVersion)
|
||||
.HasConversion(ver => ver == null ? null : ver.ToString(), str => str == null ? null : Version.Parse(str));
|
||||
b_udi
|
||||
.Property(udi => udi.LastDownloadedFormat)
|
||||
.HasConversion(af => af == null ? 0 : af.Serialize(), str => AudioFormat.Deserialize(str));
|
||||
b_udi.Property(udi => udi.LastDownloaded);
|
||||
b_udi
|
||||
.Property(udi => udi.LastDownloadedVersion)
|
||||
.HasConversion(ver => ver == null ? null : ver.ToString(), str => str == null ? null : Version.Parse(str));
|
||||
b_udi
|
||||
.Property(udi => udi.LastDownloadedFormat)
|
||||
.HasConversion(af => af == null ? 0 : af.Serialize(), str => AudioFormat.Deserialize(str));
|
||||
|
||||
b_udi.Property(udi => udi.LastDownloadedFileVersion);
|
||||
b_udi.Property(udi => udi.LastDownloadedFileVersion);
|
||||
|
||||
// owns it 1:1, store in same table
|
||||
b_udi.OwnsOne(udi => udi.Rating);
|
||||
});
|
||||
}
|
||||
}
|
||||
// owns it 1:1, store in same table
|
||||
b_udi.OwnsOne(udi => udi.Rating);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
internal class BookContributorConfig : IEntityTypeConfiguration<BookContributor>
|
||||
{
|
||||
internal class BookContributorConfig : IEntityTypeConfiguration<BookContributor>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BookContributor> entity)
|
||||
{
|
||||
entity.HasKey(bc => new { bc.BookId, bc.ContributorId, bc.Role });
|
||||
public void Configure(EntityTypeBuilder<BookContributor> entity)
|
||||
{
|
||||
entity.HasKey(bc => new { bc.BookId, bc.ContributorId, bc.Role });
|
||||
|
||||
entity.HasIndex(bc => bc.BookId);
|
||||
entity.HasIndex(bc => bc.ContributorId);
|
||||
entity.HasIndex(bc => bc.BookId);
|
||||
entity.HasIndex(bc => bc.ContributorId);
|
||||
|
||||
entity
|
||||
.HasOne(bc => bc.Book)
|
||||
.WithMany(b => b.ContributorsLink)
|
||||
.HasForeignKey(bc => bc.BookId);
|
||||
entity
|
||||
.HasOne(bc => bc.Contributor)
|
||||
.WithMany(c => c.BooksLink)
|
||||
.HasForeignKey(bc => bc.ContributorId);
|
||||
}
|
||||
}
|
||||
entity
|
||||
.HasOne(bc => bc.Book)
|
||||
.WithMany(b => b.ContributorsLink)
|
||||
.HasForeignKey(bc => bc.BookId);
|
||||
entity
|
||||
.HasOne(bc => bc.Contributor)
|
||||
.WithMany(c => c.BooksLink)
|
||||
.HasForeignKey(bc => bc.ContributorId);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
internal class CategoryConfig : IEntityTypeConfiguration<Category>
|
||||
{
|
||||
internal class CategoryConfig : IEntityTypeConfiguration<Category>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Category> entity)
|
||||
{
|
||||
entity.HasKey(c => c.CategoryId);
|
||||
entity.HasIndex(c => c.AudibleCategoryId);
|
||||
public void Configure(EntityTypeBuilder<Category> entity)
|
||||
{
|
||||
entity.HasKey(c => c.CategoryId);
|
||||
entity.HasIndex(c => c.AudibleCategoryId);
|
||||
|
||||
entity.Ignore(c => c.CategoryLadders);
|
||||
entity.Ignore(c => c.CategoryLadders);
|
||||
|
||||
entity
|
||||
.HasMany(e => e._categoryLadders)
|
||||
.WithMany(e => e._categories);
|
||||
}
|
||||
}
|
||||
entity
|
||||
.HasMany(e => e._categoryLadders)
|
||||
.WithMany(e => e._categories);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
internal class CategoryLadderConfig : IEntityTypeConfiguration<CategoryLadder>
|
||||
{
|
||||
internal class CategoryLadderConfig : IEntityTypeConfiguration<CategoryLadder>
|
||||
public void Configure(EntityTypeBuilder<CategoryLadder> entity)
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CategoryLadder> entity)
|
||||
{
|
||||
entity.HasKey(cl => cl.CategoryLadderId);
|
||||
entity.HasKey(cl => cl.CategoryLadderId);
|
||||
|
||||
entity.Ignore(cl => cl.Categories);
|
||||
entity.Ignore(cl => cl.Categories);
|
||||
|
||||
entity
|
||||
.HasMany(cl => cl._categories)
|
||||
.WithMany(c => c._categoryLadders);
|
||||
entity
|
||||
.HasMany(cl => cl._categories)
|
||||
.WithMany(c => c._categoryLadders);
|
||||
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(CategoryLadder.BooksLink))
|
||||
?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(CategoryLadder.BooksLink))
|
||||
?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
internal class ContributorConfig : IEntityTypeConfiguration<Contributor>
|
||||
{
|
||||
internal class ContributorConfig : IEntityTypeConfiguration<Contributor>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Contributor> entity)
|
||||
{
|
||||
entity.HasKey(c => c.ContributorId);
|
||||
entity.HasIndex(c => c.Name);
|
||||
public void Configure(EntityTypeBuilder<Contributor> entity)
|
||||
{
|
||||
entity.HasKey(c => c.ContributorId);
|
||||
entity.HasIndex(c => c.Name);
|
||||
|
||||
//entity.OwnsOne(b => b.AuthorProperty);
|
||||
// ... in separate table
|
||||
//entity.OwnsOne(b => b.AuthorProperty);
|
||||
// ... in separate table
|
||||
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Contributor.BooksLink))
|
||||
?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Contributor.BooksLink))
|
||||
?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs
|
||||
entity.HasData(Contributor.GetEmpty());
|
||||
}
|
||||
}
|
||||
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs
|
||||
entity.HasData(Contributor.GetEmpty());
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
internal class LibraryBookConfig : IEntityTypeConfiguration<LibraryBook>
|
||||
{
|
||||
internal class LibraryBookConfig : IEntityTypeConfiguration<LibraryBook>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<LibraryBook> entity)
|
||||
{
|
||||
// to allow same book (incl region) with diff acct.s:
|
||||
//
|
||||
// this file:
|
||||
// - composite key:
|
||||
// entity.HasKey(b => new { b.BookId, b.Account });
|
||||
// entity.HasIndex(b => b.BookId);
|
||||
// entity.HasIndex(b => b.Account);
|
||||
// - change the below relationship since Book+LibraryBook would no longer be 1:1
|
||||
//
|
||||
// other files:
|
||||
// - change Book class since Book+LibraryBook would no longer be 1:1
|
||||
// - update LibraryBook import code
|
||||
// - would likely challenge assumptions throughout Libation which have been true up until now
|
||||
public void Configure(EntityTypeBuilder<LibraryBook> entity)
|
||||
{
|
||||
// to allow same book (incl region) with diff acct.s:
|
||||
//
|
||||
// this file:
|
||||
// - composite key:
|
||||
// entity.HasKey(b => new { b.BookId, b.Account });
|
||||
// entity.HasIndex(b => b.BookId);
|
||||
// entity.HasIndex(b => b.Account);
|
||||
// - change the below relationship since Book+LibraryBook would no longer be 1:1
|
||||
//
|
||||
// other files:
|
||||
// - change Book class since Book+LibraryBook would no longer be 1:1
|
||||
// - update LibraryBook import code
|
||||
// - would likely challenge assumptions throughout Libation which have been true up until now
|
||||
|
||||
entity.HasKey(lb => lb.BookId);
|
||||
entity.HasKey(lb => lb.BookId);
|
||||
|
||||
entity
|
||||
.HasOne(lb => lb.Book)
|
||||
.WithOne()
|
||||
.HasForeignKey<LibraryBook>(lb => lb.BookId);
|
||||
}
|
||||
}
|
||||
entity
|
||||
.HasOne(lb => lb.Book)
|
||||
.WithOne()
|
||||
.HasForeignKey<LibraryBook>(lb => lb.BookId);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
internal class SeriesBookConfig : IEntityTypeConfiguration<SeriesBook>
|
||||
{
|
||||
internal class SeriesBookConfig : IEntityTypeConfiguration<SeriesBook>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SeriesBook> entity)
|
||||
{
|
||||
entity.HasKey(sb => new { sb.SeriesId, sb.BookId });
|
||||
public void Configure(EntityTypeBuilder<SeriesBook> entity)
|
||||
{
|
||||
entity.HasKey(sb => new { sb.SeriesId, sb.BookId });
|
||||
|
||||
entity.HasIndex(sb => sb.SeriesId);
|
||||
entity.HasIndex(sb => sb.BookId);
|
||||
entity.HasIndex(sb => sb.SeriesId);
|
||||
entity.HasIndex(sb => sb.BookId);
|
||||
|
||||
entity
|
||||
.HasOne(sb => sb.Series)
|
||||
.WithMany(s => s.BooksLink)
|
||||
.HasForeignKey(sb => sb.SeriesId);
|
||||
entity
|
||||
.HasOne(sb => sb.Book)
|
||||
.WithMany(b => b.SeriesLink)
|
||||
.HasForeignKey(sb => sb.BookId);
|
||||
}
|
||||
}
|
||||
entity
|
||||
.HasOne(sb => sb.Series)
|
||||
.WithMany(s => s.BooksLink)
|
||||
.HasForeignKey(sb => sb.SeriesId);
|
||||
entity
|
||||
.HasOne(sb => sb.Book)
|
||||
.WithMany(b => b.SeriesLink)
|
||||
.HasForeignKey(sb => sb.BookId);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
{
|
||||
internal class SeriesConfig : IEntityTypeConfiguration<Series>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Series> entity)
|
||||
{
|
||||
entity.HasKey(s => s.SeriesId);
|
||||
entity.HasIndex(s => s.AudibleSeriesId);
|
||||
namespace DataLayer.Configurations;
|
||||
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Series.BooksLink))
|
||||
?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
internal class SeriesConfig : IEntityTypeConfiguration<Series>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Series> entity)
|
||||
{
|
||||
entity.HasKey(s => s.SeriesId);
|
||||
entity.HasIndex(s => s.AudibleSeriesId);
|
||||
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Series.BooksLink))
|
||||
?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
@@ -1,264 +1,263 @@
|
||||
using System;
|
||||
using Dinah.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class AudibleProductId
|
||||
{
|
||||
public class AudibleProductId
|
||||
{
|
||||
public string Id { get; }
|
||||
public AudibleProductId(string id)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
// enum will be easier than bool to extend later.
|
||||
public enum ContentType
|
||||
{
|
||||
Unknown = 0,
|
||||
Product = 1,
|
||||
Episode = 2,
|
||||
Parent = 4,
|
||||
}
|
||||
|
||||
|
||||
public class Book
|
||||
{
|
||||
// implementation detail. set by db only. only used by data layer
|
||||
internal int BookId { get; private set; }
|
||||
|
||||
// immutable
|
||||
public string AudibleProductId { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Subtitle { get; private set; }
|
||||
private string? _titleWithSubtitle;
|
||||
public string TitleWithSubtitle => _titleWithSubtitle ??= string.IsNullOrEmpty(Subtitle) ? Title : $"{Title}: {Subtitle}";
|
||||
public string Description { get; private set; }
|
||||
public int LengthInMinutes { get; private set; }
|
||||
public ContentType ContentType { get; private set; }
|
||||
public string Locale { get; private set; }
|
||||
|
||||
// mutable
|
||||
public string? PictureId { get; set; }
|
||||
public string? PictureLarge { get; set; }
|
||||
|
||||
// book details
|
||||
public bool IsAbridged { get; private set; }
|
||||
public bool IsSpatial { get; private set; }
|
||||
public DateTime? DatePublished { get; private set; }
|
||||
public string? Language { get; private set; }
|
||||
|
||||
// is owned, not optional 1:1
|
||||
public UserDefinedItem UserDefinedItem { get; private set; }
|
||||
|
||||
// is owned, not optional 1:1
|
||||
/// <summary>The product's aggregate community rating</summary>
|
||||
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||
|
||||
// ef-ctor
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Book() { }
|
||||
#pragma warning restore CS8618
|
||||
// non-ef ctor
|
||||
/// <param name="audibleProductId">special id class b/c it's too easy to get string order mixed up</param>
|
||||
public Book(
|
||||
AudibleProductId audibleProductId,
|
||||
string? title,
|
||||
string? subtitle,
|
||||
string? description,
|
||||
int lengthInMinutes,
|
||||
ContentType contentType,
|
||||
IEnumerable<Contributor> authors,
|
||||
IEnumerable<Contributor> narrators,
|
||||
string localeName
|
||||
)
|
||||
{
|
||||
// validate
|
||||
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
|
||||
var productId = audibleProductId.Id;
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
|
||||
|
||||
// assign as soon as possible. stuff below relies on this
|
||||
AudibleProductId = productId;
|
||||
Locale = localeName;
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
|
||||
|
||||
// non-ef-ctor init.s
|
||||
UserDefinedItem = new UserDefinedItem(this);
|
||||
ContributorsLink = new HashSet<BookContributor>();
|
||||
CategoriesLink = new HashSet<BookCategory>();
|
||||
_seriesLink = new HashSet<SeriesBook>();
|
||||
_supplements = new HashSet<Supplement>();
|
||||
|
||||
// simple assigns
|
||||
|
||||
Title = title?.Trim() ?? "";
|
||||
Subtitle = subtitle?.Trim() ?? "";
|
||||
Description = description?.Trim() ?? "";
|
||||
LengthInMinutes = lengthInMinutes;
|
||||
ContentType = contentType;
|
||||
|
||||
// assigns with biz logic
|
||||
ReplaceAuthors(authors);
|
||||
ReplaceNarrators(narrators);
|
||||
}
|
||||
|
||||
public void UpdateTitle(string? title, string? subtitle)
|
||||
{
|
||||
Title = title?.Trim() ?? "";
|
||||
Subtitle = subtitle?.Trim() ?? "";
|
||||
_titleWithSubtitle = null;
|
||||
}
|
||||
|
||||
public void UpdateLengthInMinutes(int lengthInMinutes)
|
||||
=> LengthInMinutes = lengthInMinutes;
|
||||
|
||||
#region contributors, authors, narrators
|
||||
internal HashSet<BookContributor> ContributorsLink { get; private set; }
|
||||
|
||||
public IEnumerable<Contributor> Authors => ContributorsLink.ByRole(Role.Author).Select(bc => bc.Contributor).ToList();
|
||||
public IEnumerable<Contributor> Narrators => ContributorsLink.ByRole(Role.Narrator).Select(bc => bc.Contributor).ToList();
|
||||
public string? Publisher => ContributorsLink.ByRole(Role.Publisher).SingleOrDefault()?.Contributor.Name;
|
||||
|
||||
public void ReplaceAuthors(IEnumerable<Contributor> authors, DbContext? context = null)
|
||||
=> replaceContributors(authors, Role.Author, context);
|
||||
public void ReplaceNarrators(IEnumerable<Contributor> narrators, DbContext? context = null)
|
||||
=> replaceContributors(narrators, Role.Narrator, context);
|
||||
public void ReplacePublisher(Contributor publisher, DbContext? context = null)
|
||||
=> replaceContributors(new List<Contributor> { publisher }, Role.Publisher, context);
|
||||
private void replaceContributors(IEnumerable<Contributor> newContributors, Role role, DbContext? context = null)
|
||||
{
|
||||
ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors));
|
||||
|
||||
// the edge cases of doing local-loaded vs remote-only got weird. just load it
|
||||
if (ContributorsLink is null)
|
||||
{
|
||||
if (context is null)
|
||||
throw new ArgumentNullException(nameof(context), "A DbContext is required to load the ContributorsLink collection");
|
||||
getEntry(context).Collection(s => s.ContributorsLink).Load();
|
||||
}
|
||||
|
||||
var isIdentical
|
||||
= ContributorsLink
|
||||
!.ByRole(role)
|
||||
.Select(c => c.Contributor)
|
||||
.SequenceEqual(newContributors);
|
||||
|
||||
if (isIdentical)
|
||||
return;
|
||||
|
||||
ContributorsLink!.RemoveWhere(bc => bc.Role == role);
|
||||
addNewContributors(newContributors, role);
|
||||
}
|
||||
|
||||
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
|
||||
{
|
||||
byte order = 0;
|
||||
var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++));
|
||||
var newContributions = new HashSet<BookContributor>(newContributionsEnum);
|
||||
ContributorsLink.UnionWith(newContributions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Book> getEntry(DbContext context)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||
|
||||
var entry = context.Entry(this);
|
||||
|
||||
if (!entry.IsKeySet)
|
||||
throw new InvalidOperationException("Could not load a valid Book from database");
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
#region categories
|
||||
internal HashSet<BookCategory> CategoriesLink { get; private set; }
|
||||
|
||||
private ReadOnlyCollection<BookCategory>? _categoriesReadOnly;
|
||||
public ReadOnlyCollection<BookCategory> Categories
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_categoriesReadOnly?.SequenceEqual(CategoriesLink) is not true)
|
||||
_categoriesReadOnly = CategoriesLink.ToList().AsReadOnly();
|
||||
return _categoriesReadOnly;
|
||||
}
|
||||
}
|
||||
public void SetCategoryLadders(IEnumerable<CategoryLadder> ladders)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(ladders, nameof(ladders));
|
||||
|
||||
//Replace all existing category ladders.
|
||||
//Some books make have duplicate ladders
|
||||
CategoriesLink.Clear();
|
||||
CategoriesLink.UnionWith(ladders.Distinct().Select(l => new BookCategory(this, l)));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region series
|
||||
private HashSet<SeriesBook>? _seriesLink;
|
||||
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList() ?? [];
|
||||
|
||||
public void UpsertSeries(Series series, string? order, DbContext? context = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||
|
||||
// our add() is conditional upon what's already included in the collection.
|
||||
// therefore if not loaded, a trip is required. might as well just load it
|
||||
if (_seriesLink is null)
|
||||
{
|
||||
if (context is null)
|
||||
throw new ArgumentNullException(nameof(context), "A DbContext is required to load the SeriesLink collection");
|
||||
getEntry(context).Collection(s => s.SeriesLink).Load();
|
||||
}
|
||||
|
||||
var singleSeriesBook = _seriesLink!.SingleOrDefault(sb => sb.Series == series);
|
||||
if (singleSeriesBook is null)
|
||||
_seriesLink!.Add(new SeriesBook(series, this, order));
|
||||
else
|
||||
singleSeriesBook.UpdateOrder(order);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region supplements
|
||||
private HashSet<Supplement>? _supplements;
|
||||
public IEnumerable<Supplement> Supplements => _supplements?.ToList() ?? [];
|
||||
|
||||
public void AddSupplementDownloadUrl(string url)
|
||||
{
|
||||
// supplements are owned by Book, so no need to Load():
|
||||
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner.
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
|
||||
|
||||
if (_supplements?.Any(s => url.EqualsInsensitive(url)) is true)
|
||||
return;
|
||||
|
||||
_supplements?.Add(new Supplement(this, url));
|
||||
UserDefinedItem.PdfStatus ??= LiberatedStatus.NotLiberated;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void UpdateProductRating(float overallRating, float performanceRating, float storyRating)
|
||||
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||
|
||||
public void UpdateBookDetails(bool isAbridged, bool? isSpatial, DateTime? datePublished, string? language)
|
||||
{
|
||||
// don't overwrite with default values
|
||||
IsAbridged |= isAbridged;
|
||||
IsSpatial = isSpatial ?? IsSpatial;
|
||||
DatePublished = datePublished ?? DatePublished;
|
||||
Language = language?.FirstCharToUpper() ?? Language;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleProductId}] {TitleWithSubtitle}";
|
||||
public string Id { get; }
|
||||
public AudibleProductId(string id)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
// enum will be easier than bool to extend later.
|
||||
public enum ContentType
|
||||
{
|
||||
Unknown = 0,
|
||||
Product = 1,
|
||||
Episode = 2,
|
||||
Parent = 4,
|
||||
}
|
||||
|
||||
|
||||
public class Book
|
||||
{
|
||||
// implementation detail. set by db only. only used by data layer
|
||||
internal int BookId { get; private set; }
|
||||
|
||||
// immutable
|
||||
public string AudibleProductId { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Subtitle { get; private set; }
|
||||
private string? _titleWithSubtitle;
|
||||
public string TitleWithSubtitle => _titleWithSubtitle ??= string.IsNullOrEmpty(Subtitle) ? Title : $"{Title}: {Subtitle}";
|
||||
public string Description { get; private set; }
|
||||
public int LengthInMinutes { get; private set; }
|
||||
public ContentType ContentType { get; private set; }
|
||||
public string Locale { get; private set; }
|
||||
|
||||
// mutable
|
||||
public string? PictureId { get; set; }
|
||||
public string? PictureLarge { get; set; }
|
||||
|
||||
// book details
|
||||
public bool IsAbridged { get; private set; }
|
||||
public bool IsSpatial { get; private set; }
|
||||
public DateTime? DatePublished { get; private set; }
|
||||
public string? Language { get; private set; }
|
||||
|
||||
// is owned, not optional 1:1
|
||||
public UserDefinedItem UserDefinedItem { get; private set; }
|
||||
|
||||
// is owned, not optional 1:1
|
||||
/// <summary>The product's aggregate community rating</summary>
|
||||
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||
|
||||
// ef-ctor
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Book() { }
|
||||
#pragma warning restore CS8618
|
||||
// non-ef ctor
|
||||
/// <param name="audibleProductId">special id class b/c it's too easy to get string order mixed up</param>
|
||||
public Book(
|
||||
AudibleProductId audibleProductId,
|
||||
string? title,
|
||||
string? subtitle,
|
||||
string? description,
|
||||
int lengthInMinutes,
|
||||
ContentType contentType,
|
||||
IEnumerable<Contributor> authors,
|
||||
IEnumerable<Contributor> narrators,
|
||||
string localeName
|
||||
)
|
||||
{
|
||||
// validate
|
||||
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
|
||||
var productId = audibleProductId.Id;
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
|
||||
|
||||
// assign as soon as possible. stuff below relies on this
|
||||
AudibleProductId = productId;
|
||||
Locale = localeName;
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
|
||||
|
||||
// non-ef-ctor init.s
|
||||
UserDefinedItem = new UserDefinedItem(this);
|
||||
ContributorsLink = new HashSet<BookContributor>();
|
||||
CategoriesLink = new HashSet<BookCategory>();
|
||||
_seriesLink = new HashSet<SeriesBook>();
|
||||
_supplements = new HashSet<Supplement>();
|
||||
|
||||
// simple assigns
|
||||
|
||||
Title = title?.Trim() ?? "";
|
||||
Subtitle = subtitle?.Trim() ?? "";
|
||||
Description = description?.Trim() ?? "";
|
||||
LengthInMinutes = lengthInMinutes;
|
||||
ContentType = contentType;
|
||||
|
||||
// assigns with biz logic
|
||||
ReplaceAuthors(authors);
|
||||
ReplaceNarrators(narrators);
|
||||
}
|
||||
|
||||
public void UpdateTitle(string? title, string? subtitle)
|
||||
{
|
||||
Title = title?.Trim() ?? "";
|
||||
Subtitle = subtitle?.Trim() ?? "";
|
||||
_titleWithSubtitle = null;
|
||||
}
|
||||
|
||||
public void UpdateLengthInMinutes(int lengthInMinutes)
|
||||
=> LengthInMinutes = lengthInMinutes;
|
||||
|
||||
#region contributors, authors, narrators
|
||||
internal HashSet<BookContributor> ContributorsLink { get; private set; }
|
||||
|
||||
public IEnumerable<Contributor> Authors => ContributorsLink.ByRole(Role.Author).Select(bc => bc.Contributor).ToList();
|
||||
public IEnumerable<Contributor> Narrators => ContributorsLink.ByRole(Role.Narrator).Select(bc => bc.Contributor).ToList();
|
||||
public string? Publisher => ContributorsLink.ByRole(Role.Publisher).SingleOrDefault()?.Contributor.Name;
|
||||
|
||||
public void ReplaceAuthors(IEnumerable<Contributor> authors, DbContext? context = null)
|
||||
=> replaceContributors(authors, Role.Author, context);
|
||||
public void ReplaceNarrators(IEnumerable<Contributor> narrators, DbContext? context = null)
|
||||
=> replaceContributors(narrators, Role.Narrator, context);
|
||||
public void ReplacePublisher(Contributor publisher, DbContext? context = null)
|
||||
=> replaceContributors(new List<Contributor> { publisher }, Role.Publisher, context);
|
||||
private void replaceContributors(IEnumerable<Contributor> newContributors, Role role, DbContext? context = null)
|
||||
{
|
||||
ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors));
|
||||
|
||||
// the edge cases of doing local-loaded vs remote-only got weird. just load it
|
||||
if (ContributorsLink is null)
|
||||
{
|
||||
if (context is null)
|
||||
throw new ArgumentNullException(nameof(context), "A DbContext is required to load the ContributorsLink collection");
|
||||
getEntry(context).Collection(s => s.ContributorsLink).Load();
|
||||
}
|
||||
|
||||
var isIdentical
|
||||
= ContributorsLink
|
||||
!.ByRole(role)
|
||||
.Select(c => c.Contributor)
|
||||
.SequenceEqual(newContributors);
|
||||
|
||||
if (isIdentical)
|
||||
return;
|
||||
|
||||
ContributorsLink!.RemoveWhere(bc => bc.Role == role);
|
||||
addNewContributors(newContributors, role);
|
||||
}
|
||||
|
||||
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
|
||||
{
|
||||
byte order = 0;
|
||||
var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++));
|
||||
var newContributions = new HashSet<BookContributor>(newContributionsEnum);
|
||||
ContributorsLink.UnionWith(newContributions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Book> getEntry(DbContext context)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||
|
||||
var entry = context.Entry(this);
|
||||
|
||||
if (!entry.IsKeySet)
|
||||
throw new InvalidOperationException("Could not load a valid Book from database");
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
#region categories
|
||||
internal HashSet<BookCategory> CategoriesLink { get; private set; }
|
||||
|
||||
private ReadOnlyCollection<BookCategory>? _categoriesReadOnly;
|
||||
public ReadOnlyCollection<BookCategory> Categories
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_categoriesReadOnly?.SequenceEqual(CategoriesLink) is not true)
|
||||
_categoriesReadOnly = CategoriesLink.ToList().AsReadOnly();
|
||||
return _categoriesReadOnly;
|
||||
}
|
||||
}
|
||||
public void SetCategoryLadders(IEnumerable<CategoryLadder> ladders)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(ladders, nameof(ladders));
|
||||
|
||||
//Replace all existing category ladders.
|
||||
//Some books make have duplicate ladders
|
||||
CategoriesLink.Clear();
|
||||
CategoriesLink.UnionWith(ladders.Distinct().Select(l => new BookCategory(this, l)));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region series
|
||||
private readonly HashSet<SeriesBook>? _seriesLink;
|
||||
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList() ?? [];
|
||||
|
||||
public void UpsertSeries(Series series, string? order, DbContext? context = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||
|
||||
// our add() is conditional upon what's already included in the collection.
|
||||
// therefore if not loaded, a trip is required. might as well just load it
|
||||
if (_seriesLink is null)
|
||||
{
|
||||
if (context is null)
|
||||
throw new ArgumentNullException(nameof(context), "A DbContext is required to load the SeriesLink collection");
|
||||
getEntry(context).Collection(s => s.SeriesLink).Load();
|
||||
}
|
||||
|
||||
var singleSeriesBook = _seriesLink!.SingleOrDefault(sb => sb.Series == series);
|
||||
if (singleSeriesBook is null)
|
||||
_seriesLink!.Add(new SeriesBook(series, this, order));
|
||||
else
|
||||
singleSeriesBook.UpdateOrder(order);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region supplements
|
||||
private readonly HashSet<Supplement>? _supplements;
|
||||
public IEnumerable<Supplement> Supplements => _supplements?.ToList() ?? [];
|
||||
|
||||
public void AddSupplementDownloadUrl(string url)
|
||||
{
|
||||
// supplements are owned by Book, so no need to Load():
|
||||
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner.
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
|
||||
|
||||
if (_supplements?.Any(s => url.EqualsInsensitive(url)) is true)
|
||||
return;
|
||||
|
||||
_supplements?.Add(new Supplement(this, url));
|
||||
UserDefinedItem.PdfStatus ??= LiberatedStatus.NotLiberated;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void UpdateProductRating(float overallRating, float performanceRating, float storyRating)
|
||||
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||
|
||||
public void UpdateBookDetails(bool isAbridged, bool? isSpatial, DateTime? datePublished, string? language)
|
||||
{
|
||||
// don't overwrite with default values
|
||||
IsAbridged |= isAbridged;
|
||||
IsSpatial = isSpatial ?? IsSpatial;
|
||||
DatePublished = datePublished ?? DatePublished;
|
||||
Language = language?.FirstCharToUpper() ?? Language;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleProductId}] {TitleWithSubtitle}";
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
public class BookCategory
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
internal int CategoryLadderId { get; private set; }
|
||||
namespace DataLayer;
|
||||
|
||||
public Book Book { get; private set; }
|
||||
public CategoryLadder CategoryLadder { get; private set; }
|
||||
public class BookCategory
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
internal int CategoryLadderId { get; private set; }
|
||||
|
||||
public Book Book { get; private set; }
|
||||
public CategoryLadder CategoryLadder { get; private set; }
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private BookCategory() { }
|
||||
private BookCategory() { }
|
||||
#pragma warning restore CS8618
|
||||
|
||||
internal BookCategory(Book book, CategoryLadder categoriesList)
|
||||
{
|
||||
Book = ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
CategoryLadder = ArgumentValidator.EnsureNotNull(categoriesList, nameof(categoriesList));
|
||||
}
|
||||
internal BookCategory(Book book, CategoryLadder categoriesList)
|
||||
{
|
||||
Book = ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
CategoryLadder = ArgumentValidator.EnsureNotNull(categoriesList, nameof(categoriesList));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public enum Role { Author = 1, Narrator = 2, Publisher = 3 }
|
||||
|
||||
public class BookContributor
|
||||
{
|
||||
public enum Role { Author = 1, Narrator = 2, Publisher = 3 }
|
||||
internal int BookId { get; private set; }
|
||||
internal int ContributorId { get; private set; }
|
||||
public Role Role { get; private set; }
|
||||
public byte Order { get; private set; }
|
||||
|
||||
public class BookContributor
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
internal int ContributorId { get; private set; }
|
||||
public Role Role { get; private set; }
|
||||
public byte Order { get; private set; }
|
||||
|
||||
public Book Book { get; private set; }
|
||||
public Contributor Contributor { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
public Contributor Contributor { get; private set; }
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private BookContributor() { }
|
||||
private BookContributor() { }
|
||||
#pragma warning restore CS8618
|
||||
internal BookContributor(Book book, Contributor contributor, Role role, byte order)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
ArgumentValidator.EnsureNotNull(contributor, nameof(contributor));
|
||||
internal BookContributor(Book book, Contributor contributor, Role role, byte order)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
ArgumentValidator.EnsureNotNull(contributor, nameof(contributor));
|
||||
|
||||
Book = book;
|
||||
Contributor = contributor;
|
||||
Role = role;
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Book} {Contributor} {Role} {Order}";
|
||||
Book = book;
|
||||
Contributor = contributor;
|
||||
Role = role;
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Book} {Contributor} {Role} {Order}";
|
||||
}
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using Dinah.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class AudibleCategoryId
|
||||
{
|
||||
public class AudibleCategoryId
|
||||
{
|
||||
public string Id { get; }
|
||||
public AudibleCategoryId(string id)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
public class Category
|
||||
{
|
||||
internal int CategoryId { get; private set; }
|
||||
public string AudibleCategoryId { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
internal List<CategoryLadder> _categoryLadders = new();
|
||||
private ReadOnlyCollection<CategoryLadder>? _categoryLaddersReadOnly;
|
||||
public ReadOnlyCollection<CategoryLadder> CategoryLadders
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_categoryLaddersReadOnly?.SequenceEqual(_categoryLadders) is not true)
|
||||
_categoryLaddersReadOnly = _categoryLadders.AsReadOnly();
|
||||
return _categoryLaddersReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Category() { }
|
||||
#pragma warning restore CS8618
|
||||
/// <summary>special id class b/c it's too easy to get string order mixed up</summary>
|
||||
public Category(AudibleCategoryId audibleSeriesId, string name)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
|
||||
var id = audibleSeriesId.Id;
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
|
||||
|
||||
AudibleCategoryId = id;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleCategoryId}] {Name}";
|
||||
public string Id { get; }
|
||||
public AudibleCategoryId(string id)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
public class Category
|
||||
{
|
||||
internal int CategoryId { get; private set; }
|
||||
public string AudibleCategoryId { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
internal List<CategoryLadder> _categoryLadders = new();
|
||||
private ReadOnlyCollection<CategoryLadder>? _categoryLaddersReadOnly;
|
||||
public ReadOnlyCollection<CategoryLadder> CategoryLadders
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_categoryLaddersReadOnly?.SequenceEqual(_categoryLadders) is not true)
|
||||
_categoryLaddersReadOnly = _categoryLadders.AsReadOnly();
|
||||
return _categoryLaddersReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Category() { }
|
||||
#pragma warning restore CS8618
|
||||
/// <summary>special id class b/c it's too easy to get string order mixed up</summary>
|
||||
public Category(AudibleCategoryId audibleSeriesId, string name)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
|
||||
var id = audibleSeriesId.Id;
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
|
||||
|
||||
AudibleCategoryId = id;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleCategoryId}] {Name}";
|
||||
}
|
||||
|
||||
@@ -4,54 +4,53 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class CategoryLadder : IEquatable<CategoryLadder>
|
||||
{
|
||||
public class CategoryLadder : IEquatable<CategoryLadder>
|
||||
internal int CategoryLadderId { get; private set; }
|
||||
|
||||
internal List<Category> _categories;
|
||||
private ReadOnlyCollection<Category>? _categoriesReadOnly;
|
||||
public ReadOnlyCollection<Category> Categories
|
||||
{
|
||||
internal int CategoryLadderId { get; private set; }
|
||||
|
||||
internal List<Category> _categories;
|
||||
private ReadOnlyCollection<Category>? _categoriesReadOnly;
|
||||
public ReadOnlyCollection<Category> Categories
|
||||
get
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_categoriesReadOnly?.SequenceEqual(_categories) is not true)
|
||||
_categoriesReadOnly = _categories.AsReadOnly();
|
||||
return _categoriesReadOnly;
|
||||
}
|
||||
if (_categoriesReadOnly?.SequenceEqual(_categories) is not true)
|
||||
_categoriesReadOnly = _categories.AsReadOnly();
|
||||
return _categoriesReadOnly;
|
||||
}
|
||||
|
||||
private HashSet<BookCategory>? _booksLink;
|
||||
public IEnumerable<BookCategory>? BooksLink => _booksLink?.ToList();
|
||||
private CategoryLadder() { _categories = new(); }
|
||||
public CategoryLadder(List<Category> categories)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(categories, nameof(categories));
|
||||
ArgumentValidator.EnsureGreaterThan(categories.Count, nameof(categories), 0);
|
||||
_booksLink = new HashSet<BookCategory>();
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
HashCode hashCode = default;
|
||||
foreach (var category in _categories)
|
||||
hashCode.Add(category.AudibleCategoryId);
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
|
||||
public bool Equals(CategoryLadder? other)
|
||||
=> other?._categories is not null
|
||||
&& Equals(other._categories.Select(c => c.AudibleCategoryId));
|
||||
|
||||
public bool Equals(IEnumerable<string?>? categoryIds)
|
||||
=> categoryIds is not null
|
||||
&& _categories.Select(c => c.AudibleCategoryId).SequenceEqual(categoryIds);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is CategoryLadder other && Equals(other);
|
||||
|
||||
public override string ToString() => string.Join(" > ", _categories.Select(c => c.Name));
|
||||
}
|
||||
|
||||
private readonly HashSet<BookCategory>? _booksLink;
|
||||
public IEnumerable<BookCategory>? BooksLink => _booksLink?.ToList();
|
||||
private CategoryLadder() { _categories = new(); }
|
||||
public CategoryLadder(List<Category> categories)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(categories, nameof(categories));
|
||||
ArgumentValidator.EnsureGreaterThan(categories.Count, nameof(categories), 0);
|
||||
_booksLink = new HashSet<BookCategory>();
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
HashCode hashCode = default;
|
||||
foreach (var category in _categories)
|
||||
hashCode.Add(category.AudibleCategoryId);
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
|
||||
public bool Equals(CategoryLadder? other)
|
||||
=> other?._categories is not null
|
||||
&& Equals(other._categories.Select(c => c.AudibleCategoryId));
|
||||
|
||||
public bool Equals(IEnumerable<string?>? categoryIds)
|
||||
=> categoryIds is not null
|
||||
&& _categories.Select(c => c.AudibleCategoryId).SequenceEqual(categoryIds);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is CategoryLadder other && Equals(other);
|
||||
|
||||
public override string ToString() => string.Join(" > ", _categories.Select(c => c.Name));
|
||||
}
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using Dinah.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class Contributor
|
||||
{
|
||||
public class Contributor
|
||||
{
|
||||
// Empty is a special case. use private ctor w/o validation
|
||||
public static Contributor GetEmpty() => new() { ContributorId = -1, Name = "" };
|
||||
// Empty is a special case. use private ctor w/o validation
|
||||
public static Contributor GetEmpty() => new() { ContributorId = -1, Name = "" };
|
||||
|
||||
// contributors search links are just name with url-encoding. space can be + or %20
|
||||
// author search link: /search?searchAuthor=Robert+Bevan
|
||||
// narrator search link: /search?searchNarrator=Robert+Bevan
|
||||
// can also search multiples. concat with comma before url encode
|
||||
// contributors search links are just name with url-encoding. space can be + or %20
|
||||
// author search link: /search?searchAuthor=Robert+Bevan
|
||||
// narrator search link: /search?searchNarrator=Robert+Bevan
|
||||
// can also search multiples. concat with comma before url encode
|
||||
|
||||
// id.s
|
||||
// ----
|
||||
// https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2
|
||||
// goes to summary page
|
||||
// at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman
|
||||
// some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears
|
||||
// all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman
|
||||
// id.s
|
||||
// ----
|
||||
// https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2
|
||||
// goes to summary page
|
||||
// at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman
|
||||
// some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears
|
||||
// all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman
|
||||
|
||||
internal int ContributorId { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
internal int ContributorId { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
private HashSet<BookContributor> _booksLink;
|
||||
public IEnumerable<BookContributor> BooksLink => _booksLink?.ToList() ?? [];
|
||||
private readonly HashSet<BookContributor> _booksLink;
|
||||
public IEnumerable<BookContributor> BooksLink => _booksLink?.ToList() ?? [];
|
||||
|
||||
public string? AudibleContributorId { get; private set; }
|
||||
public string? AudibleContributorId { get; private set; }
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Contributor() { }
|
||||
private Contributor() { }
|
||||
#pragma warning restore CS8618
|
||||
public Contributor(string name, string? audibleContributorId = null)
|
||||
{
|
||||
Name = ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
|
||||
public Contributor(string name, string? audibleContributorId = null)
|
||||
{
|
||||
Name = ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
|
||||
|
||||
_booksLink = new HashSet<BookContributor>();
|
||||
SetAudibleContributorId(audibleContributorId);
|
||||
}
|
||||
_booksLink = new HashSet<BookContributor>();
|
||||
SetAudibleContributorId(audibleContributorId);
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
public void SetAudibleContributorId(string? audibleContributorId)
|
||||
{
|
||||
// don't overwrite with null or whitespace but not an error
|
||||
if (!string.IsNullOrWhiteSpace(audibleContributorId))
|
||||
AudibleContributorId = audibleContributorId;
|
||||
}
|
||||
public override string ToString() => Name;
|
||||
public void SetAudibleContributorId(string? audibleContributorId)
|
||||
{
|
||||
// don't overwrite with null or whitespace but not an error
|
||||
if (!string.IsNullOrWhiteSpace(audibleContributorId))
|
||||
AudibleContributorId = audibleContributorId;
|
||||
}
|
||||
|
||||
public bool IsEmpty => ContributorId == -1;
|
||||
}
|
||||
public bool IsEmpty => ContributorId == -1;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
using System;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class LibraryBook
|
||||
{
|
||||
public class LibraryBook
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
internal int BookId { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
|
||||
public DateTime DateAdded { get; private set; }
|
||||
public string Account { get; private set; }
|
||||
public DateTime DateAdded { get; private set; }
|
||||
public string Account { get; private set; }
|
||||
|
||||
public bool IsDeleted { get; set; }
|
||||
public bool AbsentFromLastScan { get; set; }
|
||||
|
||||
public DateTime? IncludedUntil { get; private set; }
|
||||
public bool IsAudiblePlus { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public bool AbsentFromLastScan { get; set; }
|
||||
|
||||
public DateTime? IncludedUntil { get; private set; }
|
||||
public bool IsAudiblePlus { get; set; }
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private LibraryBook() { }
|
||||
private LibraryBook() { }
|
||||
#pragma warning restore CS8618
|
||||
public LibraryBook(Book book, DateTime dateAdded, string account)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
public LibraryBook(Book book, DateTime dateAdded, string account)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
|
||||
Book = book;
|
||||
DateAdded = dateAdded;
|
||||
Account = account;
|
||||
}
|
||||
Book = book;
|
||||
DateAdded = dateAdded;
|
||||
Account = account;
|
||||
}
|
||||
|
||||
public void SetAccount(string account) => Account = account;
|
||||
public void SetIncludedUntil(DateTime? includedUntil) => IncludedUntil = includedUntil;
|
||||
public void SetIsAudiblePlus(bool isAudiblePlus) => IsAudiblePlus = isAudiblePlus;
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
}
|
||||
public void SetAccount(string account) => Account = account;
|
||||
public void SetIncludedUntil(DateTime? includedUntil) => IncludedUntil = includedUntil;
|
||||
public void SetIsAudiblePlus(bool isAudiblePlus) => IsAudiblePlus = isAudiblePlus;
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
}
|
||||
@@ -1,45 +1,44 @@
|
||||
using System;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
/// <summary>Parameterless ctor and setters should be used by EF only. Everything else should treat it as immutable</summary>
|
||||
public record Rating : IComparable<Rating>, IComparable
|
||||
{
|
||||
/// <summary>Parameterless ctor and setters should be used by EF only. Everything else should treat it as immutable</summary>
|
||||
public record Rating : IComparable<Rating>, IComparable
|
||||
{
|
||||
public float OverallRating { get; private set; }
|
||||
public float PerformanceRating { get; private set; }
|
||||
public float StoryRating { get; private set; }
|
||||
public float OverallRating { get; private set; }
|
||||
public float PerformanceRating { get; private set; }
|
||||
public float StoryRating { get; private set; }
|
||||
|
||||
private Rating() { }
|
||||
public Rating(float overallRating, float performanceRating, float storyRating)
|
||||
{
|
||||
OverallRating = overallRating;
|
||||
PerformanceRating = performanceRating;
|
||||
StoryRating = storyRating;
|
||||
}
|
||||
private Rating() { }
|
||||
public Rating(float overallRating, float performanceRating, float storyRating)
|
||||
{
|
||||
OverallRating = overallRating;
|
||||
PerformanceRating = performanceRating;
|
||||
StoryRating = storyRating;
|
||||
}
|
||||
|
||||
// EF magically tracks this owned object. by replacing it with a new() immutable object, stuff gets weird. update instead
|
||||
internal void Update(float overallRating, float performanceRating, float storyRating)
|
||||
{
|
||||
// don't overwrite with all 0
|
||||
if (overallRating + performanceRating + storyRating == 0)
|
||||
return;
|
||||
// EF magically tracks this owned object. by replacing it with a new() immutable object, stuff gets weird. update instead
|
||||
internal void Update(float overallRating, float performanceRating, float storyRating)
|
||||
{
|
||||
// don't overwrite with all 0
|
||||
if (overallRating + performanceRating + storyRating == 0)
|
||||
return;
|
||||
|
||||
OverallRating = overallRating;
|
||||
PerformanceRating = performanceRating;
|
||||
StoryRating = storyRating;
|
||||
}
|
||||
OverallRating = overallRating;
|
||||
PerformanceRating = performanceRating;
|
||||
StoryRating = storyRating;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
||||
|
||||
public int CompareTo(Rating? other)
|
||||
{
|
||||
if (other is null) return 1;
|
||||
var compare = OverallRating.CompareTo(other.OverallRating);
|
||||
if (compare != 0) return compare;
|
||||
compare = PerformanceRating.CompareTo(other.PerformanceRating);
|
||||
if (compare != 0) return compare;
|
||||
return StoryRating.CompareTo(other.StoryRating);
|
||||
}
|
||||
public int CompareTo(object? obj) => obj is Rating second ? CompareTo(second) : 1;
|
||||
}
|
||||
public int CompareTo(Rating? other)
|
||||
{
|
||||
if (other is null) return 1;
|
||||
var compare = OverallRating.CompareTo(other.OverallRating);
|
||||
if (compare != 0) return compare;
|
||||
compare = PerformanceRating.CompareTo(other.PerformanceRating);
|
||||
if (compare != 0) return compare;
|
||||
return StoryRating.CompareTo(other.StoryRating);
|
||||
}
|
||||
public int CompareTo(object? obj) => obj is Rating second ? CompareTo(second) : 1;
|
||||
}
|
||||
|
||||
@@ -1,54 +1,52 @@
|
||||
using System;
|
||||
using Dinah.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class AudibleSeriesId
|
||||
{
|
||||
public class AudibleSeriesId
|
||||
{
|
||||
public string Id { get; }
|
||||
public AudibleSeriesId(string id)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
public class Series
|
||||
{
|
||||
internal int SeriesId { get; private set; }
|
||||
public string AudibleSeriesId { get; private set; }
|
||||
|
||||
/// <summary>optional</summary>
|
||||
public string? Name { get; private set; }
|
||||
|
||||
private HashSet<SeriesBook> _booksLink;
|
||||
public IEnumerable<SeriesBook> BooksLink
|
||||
=> _booksLink?
|
||||
.OrderBy(sb => sb.Index)
|
||||
.ToList() ?? [];
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Series() { }
|
||||
#pragma warning restore CS8618
|
||||
/// <summary>special id class b/c it's too easy to get string order mixed up</summary>
|
||||
public Series(AudibleSeriesId audibleSeriesId, string? name = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
|
||||
var id = audibleSeriesId.Id;
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
AudibleSeriesId = id;
|
||||
_booksLink = new HashSet<SeriesBook>();
|
||||
UpdateName(name);
|
||||
}
|
||||
|
||||
public void UpdateName(string? name)
|
||||
{
|
||||
// don't overwrite with null or whitespace but not an error
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string? ToString() => Name;
|
||||
public string Id { get; }
|
||||
public AudibleSeriesId(string id)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
public class Series
|
||||
{
|
||||
internal int SeriesId { get; private set; }
|
||||
public string AudibleSeriesId { get; private set; }
|
||||
|
||||
/// <summary>optional</summary>
|
||||
public string? Name { get; private set; }
|
||||
|
||||
private readonly HashSet<SeriesBook> _booksLink;
|
||||
public IEnumerable<SeriesBook> BooksLink
|
||||
=> _booksLink?
|
||||
.OrderBy(sb => sb.Index)
|
||||
.ToList() ?? [];
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Series() { }
|
||||
#pragma warning restore CS8618
|
||||
/// <summary>special id class b/c it's too easy to get string order mixed up</summary>
|
||||
public Series(AudibleSeriesId audibleSeriesId, string? name = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
|
||||
var id = audibleSeriesId.Id;
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||
AudibleSeriesId = id;
|
||||
_booksLink = new HashSet<SeriesBook>();
|
||||
UpdateName(name);
|
||||
}
|
||||
|
||||
public void UpdateName(string? name)
|
||||
{
|
||||
// don't overwrite with null or whitespace but not an error
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string? ToString() => Name;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class SeriesBook
|
||||
{
|
||||
public class SeriesBook
|
||||
{
|
||||
internal int SeriesId { get; private set; }
|
||||
internal int BookId { get; private set; }
|
||||
internal int SeriesId { get; private set; }
|
||||
internal int BookId { get; private set; }
|
||||
|
||||
public string? Order { get; private set; }
|
||||
public float Index => StringLib.ExtractFirstNumber(Order);
|
||||
public string? Order { get; private set; }
|
||||
public float Index => StringLib.ExtractFirstNumber(Order);
|
||||
|
||||
public Series Series { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
public Series Series { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private SeriesBook() { }
|
||||
private SeriesBook() { }
|
||||
#pragma warning restore CS8618
|
||||
internal SeriesBook(Series series, Book book, string? order)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
internal SeriesBook(Series series, Book book, string? order)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
|
||||
Series = series;
|
||||
Book = book;
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public void UpdateOrder(string? order)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(order))
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Series={Series} Book={Book}";
|
||||
Series = series;
|
||||
Book = book;
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public void UpdateOrder(string? order)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(order))
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Series={Series} Book={Book}";
|
||||
}
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
/// <summary>PDF/ZIP files only. Although book download info could be the same format, they're substantially different and subject to change</summary>
|
||||
public class Supplement
|
||||
{
|
||||
internal int SupplementId { get; private set; }
|
||||
internal int BookId { get; private set; }
|
||||
namespace DataLayer;
|
||||
|
||||
public Book Book { get; private set; }
|
||||
public string Url { get; private set; }
|
||||
/// <summary>PDF/ZIP files only. Although book download info could be the same format, they're substantially different and subject to change</summary>
|
||||
public class Supplement
|
||||
{
|
||||
internal int SupplementId { get; private set; }
|
||||
internal int BookId { get; private set; }
|
||||
|
||||
public Book Book { get; private set; }
|
||||
public string Url { get; private set; }
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Supplement() { }
|
||||
private Supplement() { }
|
||||
#pragma warning restore CS8618
|
||||
public Supplement(Book book, string url)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
|
||||
public Supplement(Book book, string url)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
|
||||
|
||||
Book = book;
|
||||
Url = url;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Book} {Url.Substring(Url.Length - 4)}";
|
||||
Book = book;
|
||||
Url = url;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Book} {Url.Substring(Url.Length - 4)}";
|
||||
}
|
||||
|
||||
@@ -1,250 +1,249 @@
|
||||
using System;
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
/// <summary>
|
||||
/// Do not track in-process state. In-process state is determined by the presence of temp file.
|
||||
/// </summary>
|
||||
public enum LiberatedStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not track in-process state. In-process state is determined by the presence of temp file.
|
||||
/// </summary>
|
||||
public enum LiberatedStatus
|
||||
{
|
||||
NotLiberated = 0,
|
||||
Liberated = 1,
|
||||
/// <summary>Error occurred during liberation. Don't retry</summary>
|
||||
Error = 2,
|
||||
NotLiberated = 0,
|
||||
Liberated = 1,
|
||||
/// <summary>Error occurred during liberation. Don't retry</summary>
|
||||
Error = 2,
|
||||
|
||||
/// <summary>Application-state only. Not a valid persistence state.</summary>
|
||||
PartialDownload = 0x1000
|
||||
}
|
||||
|
||||
public partial class UserDefinedItem
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
/// <summary>
|
||||
/// Date the audio file was last downloaded.
|
||||
/// </summary>
|
||||
public DateTime? LastDownloaded { get; private set; }
|
||||
/// <summary>
|
||||
/// Version of Libation used the last time the audio file was downloaded.
|
||||
/// </summary>
|
||||
public Version? LastDownloadedVersion { get; private set; }
|
||||
/// <summary>
|
||||
/// Audio format of the last downloaded audio file.
|
||||
/// </summary>
|
||||
public AudioFormat? LastDownloadedFormat { get; private set; }
|
||||
/// <summary>
|
||||
/// Version of the audio file that was last downloaded.
|
||||
/// </summary>
|
||||
public string? LastDownloadedFileVersion { get; private set; }
|
||||
|
||||
public void SetLastDownloaded(Version? libationVersion, AudioFormat? audioFormat, string? audioVersion)
|
||||
{
|
||||
if (LastDownloadedVersion != libationVersion)
|
||||
{
|
||||
LastDownloadedVersion = libationVersion;
|
||||
OnItemChanged(nameof(LastDownloadedVersion));
|
||||
}
|
||||
if (LastDownloadedFormat != audioFormat)
|
||||
{
|
||||
LastDownloadedFormat = audioFormat;
|
||||
OnItemChanged(nameof(LastDownloadedFormat));
|
||||
}
|
||||
if (LastDownloadedFileVersion != audioVersion)
|
||||
{
|
||||
LastDownloadedFileVersion = audioVersion;
|
||||
OnItemChanged(nameof(LastDownloadedFileVersion));
|
||||
}
|
||||
|
||||
if (libationVersion is null)
|
||||
{
|
||||
LastDownloaded = null;
|
||||
LastDownloadedFormat = null;
|
||||
LastDownloadedFileVersion = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
LastDownloaded = DateTime.Now;
|
||||
OnItemChanged(nameof(LastDownloaded));
|
||||
}
|
||||
}
|
||||
private UserDefinedItem()
|
||||
{
|
||||
// for EF
|
||||
Book = null!;
|
||||
}
|
||||
|
||||
internal UserDefinedItem(Book book)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
Book = book;
|
||||
}
|
||||
|
||||
#region Tags
|
||||
private string _tags = "";
|
||||
public string Tags
|
||||
{
|
||||
get => _tags;
|
||||
set
|
||||
{
|
||||
var newTags = sanitize(value);
|
||||
if (_tags != newTags)
|
||||
{
|
||||
_tags = newTags;
|
||||
OnItemChanged(nameof(Tags));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
|
||||
|
||||
/// <summary>
|
||||
/// only legal chars are letters numbers underscores and separating whitespace
|
||||
///
|
||||
/// technically, the only char.s which aren't easily supported are \ [ ]
|
||||
/// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
|
||||
/// it's easy to expand whitelist as needed
|
||||
/// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
|
||||
///
|
||||
/// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
|
||||
/// full list of characters which must be escaped:
|
||||
/// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
|
||||
/// </summary>
|
||||
|
||||
[GeneratedRegex(@"[^\w\d\s_]")]
|
||||
private static partial Regex IllegalCharacterRegex();
|
||||
private static string sanitize(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return "";
|
||||
|
||||
var str = input
|
||||
.Trim()
|
||||
.ToLowerInvariant()
|
||||
// assume a hyphen is supposed to be an underscore
|
||||
.Replace("-", "_");
|
||||
|
||||
var unique = IllegalCharacterRegex()
|
||||
// turn illegal characters into a space. this will also take care of turning new lines into spaces
|
||||
.Replace(str, " ")
|
||||
// split and remove excess spaces
|
||||
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
// de-dup
|
||||
.Distinct()
|
||||
// this will prevent order from being relevant
|
||||
.OrderBy(a => a);
|
||||
|
||||
// currently, the string is the canonical set. if we later make the collection into the canonical set:
|
||||
// var tags = new Hashset<string>(list); // de-dup, order doesn't matter but can seem random due to hashing algo
|
||||
// var isEqual = tagsNew.SetEquals(tagsOld);
|
||||
|
||||
return string.Join(" ", unique);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Rating
|
||||
// owned: not an optional one-to-one
|
||||
/// <summary>The user's individual book rating</summary>
|
||||
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||
|
||||
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
|
||||
{
|
||||
var changed = Rating.OverallRating != overallRating || Rating.PerformanceRating != performanceRating || Rating.StoryRating != storyRating;
|
||||
Rating.Update(overallRating, performanceRating, storyRating);
|
||||
if (changed) OnItemChanged(nameof(Rating));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LiberatedStatuses
|
||||
/// <summary>
|
||||
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
|
||||
/// This signals the change of the in-memory value; it does not ensure that the new value has been persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<string>? ItemChanged;
|
||||
|
||||
private void OnItemChanged(string e)
|
||||
{
|
||||
// HACK
|
||||
// must not fire during initial import.
|
||||
//
|
||||
// these checks are necessary because current architecture attaches to this instead of attaching to an event *after* fully committed to db. the attached delegate/action sometimes calls commit:
|
||||
//
|
||||
// desired:
|
||||
// - importing new book
|
||||
// - update pdf status
|
||||
// - initial book commit
|
||||
//
|
||||
// actual without these checks [BAD]:
|
||||
// - importing new book
|
||||
// - update pdf status
|
||||
// - invoke event
|
||||
// - commit UserDefinedItem
|
||||
// - initial book commit
|
||||
if (BookId > 0 && Book is not null && Book.BookId > 0)
|
||||
ItemChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private LiberatedStatus _bookStatus;
|
||||
private LiberatedStatus? _pdfStatus;
|
||||
public LiberatedStatus BookStatus
|
||||
{
|
||||
get => _bookStatus;
|
||||
set
|
||||
{
|
||||
// PartialDownload is a live/ephemeral status, not a persistent one. Do not store
|
||||
var displayStatus = value == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : value;
|
||||
if (_bookStatus != displayStatus)
|
||||
{
|
||||
_bookStatus = displayStatus;
|
||||
OnItemChanged(nameof(BookStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
public void SetPdfStatus(LiberatedStatus? pdfStatus)
|
||||
{
|
||||
// don't change whether pdf is actually available. if null, leave as null. if not null, only assign non-null
|
||||
|
||||
// null => non-null : only when adding a supplement
|
||||
|
||||
if (pdfStatus.HasValue && PdfStatus.HasValue)
|
||||
PdfStatus = pdfStatus;
|
||||
}
|
||||
public LiberatedStatus? PdfStatus
|
||||
{
|
||||
get => _pdfStatus;
|
||||
internal set
|
||||
{
|
||||
if (_pdfStatus != value)
|
||||
{
|
||||
_pdfStatus = value;
|
||||
OnItemChanged(nameof(PdfStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IsFinished
|
||||
private bool _isFinished;
|
||||
public bool IsFinished
|
||||
{
|
||||
get => _isFinished;
|
||||
set
|
||||
{
|
||||
if (value != _isFinished)
|
||||
{
|
||||
_isFinished = value;
|
||||
OnItemChanged(nameof(IsFinished));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override string ToString() => $"{Book} {Rating} {Tags}";
|
||||
}
|
||||
/// <summary>Application-state only. Not a valid persistence state.</summary>
|
||||
PartialDownload = 0x1000
|
||||
}
|
||||
|
||||
public partial class UserDefinedItem
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
/// <summary>
|
||||
/// Date the audio file was last downloaded.
|
||||
/// </summary>
|
||||
public DateTime? LastDownloaded { get; private set; }
|
||||
/// <summary>
|
||||
/// Version of Libation used the last time the audio file was downloaded.
|
||||
/// </summary>
|
||||
public Version? LastDownloadedVersion { get; private set; }
|
||||
/// <summary>
|
||||
/// Audio format of the last downloaded audio file.
|
||||
/// </summary>
|
||||
public AudioFormat? LastDownloadedFormat { get; private set; }
|
||||
/// <summary>
|
||||
/// Version of the audio file that was last downloaded.
|
||||
/// </summary>
|
||||
public string? LastDownloadedFileVersion { get; private set; }
|
||||
|
||||
public void SetLastDownloaded(Version? libationVersion, AudioFormat? audioFormat, string? audioVersion)
|
||||
{
|
||||
if (LastDownloadedVersion != libationVersion)
|
||||
{
|
||||
LastDownloadedVersion = libationVersion;
|
||||
OnItemChanged(nameof(LastDownloadedVersion));
|
||||
}
|
||||
if (LastDownloadedFormat != audioFormat)
|
||||
{
|
||||
LastDownloadedFormat = audioFormat;
|
||||
OnItemChanged(nameof(LastDownloadedFormat));
|
||||
}
|
||||
if (LastDownloadedFileVersion != audioVersion)
|
||||
{
|
||||
LastDownloadedFileVersion = audioVersion;
|
||||
OnItemChanged(nameof(LastDownloadedFileVersion));
|
||||
}
|
||||
|
||||
if (libationVersion is null)
|
||||
{
|
||||
LastDownloaded = null;
|
||||
LastDownloadedFormat = null;
|
||||
LastDownloadedFileVersion = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
LastDownloaded = DateTime.Now;
|
||||
OnItemChanged(nameof(LastDownloaded));
|
||||
}
|
||||
}
|
||||
private UserDefinedItem()
|
||||
{
|
||||
// for EF
|
||||
Book = null!;
|
||||
}
|
||||
|
||||
internal UserDefinedItem(Book book)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
Book = book;
|
||||
}
|
||||
|
||||
#region Tags
|
||||
private string _tags = "";
|
||||
public string Tags
|
||||
{
|
||||
get => _tags;
|
||||
set
|
||||
{
|
||||
var newTags = sanitize(value);
|
||||
if (_tags != newTags)
|
||||
{
|
||||
_tags = newTags;
|
||||
OnItemChanged(nameof(Tags));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
|
||||
|
||||
/// <summary>
|
||||
/// only legal chars are letters numbers underscores and separating whitespace
|
||||
///
|
||||
/// technically, the only char.s which aren't easily supported are \ [ ]
|
||||
/// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
|
||||
/// it's easy to expand whitelist as needed
|
||||
/// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
|
||||
///
|
||||
/// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
|
||||
/// full list of characters which must be escaped:
|
||||
/// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
|
||||
/// </summary>
|
||||
|
||||
[GeneratedRegex(@"[^\w\d\s_]")]
|
||||
private static partial Regex IllegalCharacterRegex();
|
||||
private static string sanitize(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return "";
|
||||
|
||||
var str = input
|
||||
.Trim()
|
||||
.ToLowerInvariant()
|
||||
// assume a hyphen is supposed to be an underscore
|
||||
.Replace("-", "_");
|
||||
|
||||
var unique = IllegalCharacterRegex()
|
||||
// turn illegal characters into a space. this will also take care of turning new lines into spaces
|
||||
.Replace(str, " ")
|
||||
// split and remove excess spaces
|
||||
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
// de-dup
|
||||
.Distinct()
|
||||
// this will prevent order from being relevant
|
||||
.OrderBy(a => a);
|
||||
|
||||
// currently, the string is the canonical set. if we later make the collection into the canonical set:
|
||||
// var tags = new Hashset<string>(list); // de-dup, order doesn't matter but can seem random due to hashing algo
|
||||
// var isEqual = tagsNew.SetEquals(tagsOld);
|
||||
|
||||
return string.Join(" ", unique);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Rating
|
||||
// owned: not an optional one-to-one
|
||||
/// <summary>The user's individual book rating</summary>
|
||||
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||
|
||||
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
|
||||
{
|
||||
var changed = Rating.OverallRating != overallRating || Rating.PerformanceRating != performanceRating || Rating.StoryRating != storyRating;
|
||||
Rating.Update(overallRating, performanceRating, storyRating);
|
||||
if (changed) OnItemChanged(nameof(Rating));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LiberatedStatuses
|
||||
/// <summary>
|
||||
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
|
||||
/// This signals the change of the in-memory value; it does not ensure that the new value has been persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<string>? ItemChanged;
|
||||
|
||||
private void OnItemChanged(string e)
|
||||
{
|
||||
// HACK
|
||||
// must not fire during initial import.
|
||||
//
|
||||
// these checks are necessary because current architecture attaches to this instead of attaching to an event *after* fully committed to db. the attached delegate/action sometimes calls commit:
|
||||
//
|
||||
// desired:
|
||||
// - importing new book
|
||||
// - update pdf status
|
||||
// - initial book commit
|
||||
//
|
||||
// actual without these checks [BAD]:
|
||||
// - importing new book
|
||||
// - update pdf status
|
||||
// - invoke event
|
||||
// - commit UserDefinedItem
|
||||
// - initial book commit
|
||||
if (BookId > 0 && Book is not null && Book.BookId > 0)
|
||||
ItemChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private LiberatedStatus _bookStatus;
|
||||
private LiberatedStatus? _pdfStatus;
|
||||
public LiberatedStatus BookStatus
|
||||
{
|
||||
get => _bookStatus;
|
||||
set
|
||||
{
|
||||
// PartialDownload is a live/ephemeral status, not a persistent one. Do not store
|
||||
var displayStatus = value == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : value;
|
||||
if (_bookStatus != displayStatus)
|
||||
{
|
||||
_bookStatus = displayStatus;
|
||||
OnItemChanged(nameof(BookStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
public void SetPdfStatus(LiberatedStatus? pdfStatus)
|
||||
{
|
||||
// don't change whether pdf is actually available. if null, leave as null. if not null, only assign non-null
|
||||
|
||||
// null => non-null : only when adding a supplement
|
||||
|
||||
if (pdfStatus.HasValue && PdfStatus.HasValue)
|
||||
PdfStatus = pdfStatus;
|
||||
}
|
||||
public LiberatedStatus? PdfStatus
|
||||
{
|
||||
get => _pdfStatus;
|
||||
internal set
|
||||
{
|
||||
if (_pdfStatus != value)
|
||||
{
|
||||
_pdfStatus = value;
|
||||
OnItemChanged(nameof(PdfStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IsFinished
|
||||
private bool _isFinished;
|
||||
public bool IsFinished
|
||||
{
|
||||
get => _isFinished;
|
||||
set
|
||||
{
|
||||
if (value != _isFinished)
|
||||
{
|
||||
_isFinished = value;
|
||||
OnItemChanged(nameof(IsFinished));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override string ToString() => $"{Book} {Rating} {Tags}";
|
||||
}
|
||||
|
||||
@@ -1,136 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public static class EntityExtensions
|
||||
{
|
||||
public static class EntityExtensions
|
||||
public static IEnumerable<BookContributor> ByRole(this IEnumerable<BookContributor> contributors, Role role)
|
||||
=> contributors
|
||||
.Where(a => a.Role == role)
|
||||
.OrderBy(a => a.Order);
|
||||
|
||||
|
||||
extension(Book book)
|
||||
{
|
||||
public static IEnumerable<BookContributor> ByRole(this IEnumerable<BookContributor> contributors, Role role)
|
||||
=> contributors
|
||||
.Where(a => a.Role == role)
|
||||
.OrderBy(a => a.Order);
|
||||
public string SeriesSortable() => Formatters.GetSortName(book.SeriesNames(true));
|
||||
public string TitleSortable() => Formatters.GetSortName(book.Title + book.Subtitle);
|
||||
|
||||
public string AuthorNames => string.Join(", ", book.Authors.Select(a => a.Name));
|
||||
public string NarratorNames => string.Join(", ", book.Narrators.Select(n => n.Name));
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public bool AudioExists => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated or LiberatedStatus.Error;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public bool PdfExists => book.UserDefinedItem.PdfStatus is LiberatedStatus.Liberated;
|
||||
/// <summary> Whether the book has any supplements </summary>
|
||||
public bool HasPdf => book.Supplements.Any();
|
||||
|
||||
extension(Book book)
|
||||
public string SeriesNames(bool includeIndex = false)
|
||||
{
|
||||
public string SeriesSortable() => Formatters.GetSortName(book.SeriesNames(true));
|
||||
public string TitleSortable() => Formatters.GetSortName(book.Title + book.Subtitle);
|
||||
|
||||
public string AuthorNames => string.Join(", ", book.Authors.Select(a => a.Name));
|
||||
public string NarratorNames => string.Join(", ", book.Narrators.Select(n => n.Name));
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public bool AudioExists => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated or LiberatedStatus.Error;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public bool PdfExists => book.UserDefinedItem.PdfStatus is LiberatedStatus.Liberated;
|
||||
/// <summary> Whether the book has any supplements </summary>
|
||||
public bool HasPdf => book.Supplements.Any();
|
||||
|
||||
public string SeriesNames(bool includeIndex = false)
|
||||
{
|
||||
if (book.SeriesLink is null)
|
||||
return "";
|
||||
|
||||
// first: alphabetical by name
|
||||
var withNames = book.SeriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(getSeriesNameString)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// then un-named are alpha by series id
|
||||
var nullNames = book.SeriesLink
|
||||
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(s => s.Series.AudibleSeriesId)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
|
||||
var all = withNames.Union(nullNames).ToList();
|
||||
return string.Join(", ", all);
|
||||
|
||||
string getSeriesNameString(SeriesBook sb)
|
||||
=> includeIndex && !string.IsNullOrWhiteSpace(sb.Order) && sb.Order != "-1"
|
||||
? $"{sb.Series.Name} (#{sb.Order})"
|
||||
: sb.Series.Name ?? "";
|
||||
}
|
||||
|
||||
public string[] LowestCategoryNames()
|
||||
=> book.CategoriesLink?.Count is 0 or null ? []
|
||||
: book
|
||||
.CategoriesLink
|
||||
.Select(cl => cl.CategoryLadder.Categories.LastOrDefault()?.Name)
|
||||
.OfType<string>()
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
public string[] AllCategoryNames()
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.Name)
|
||||
.ToArray();
|
||||
|
||||
public string[]? AllCategoryIds()
|
||||
=> book.CategoriesLink?.Count is null or 0 ? null
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.AudibleCategoryId)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
||||
public static string AggregateTitles(this IEnumerable<LibraryBook> libraryBooks, int max = 5)
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
if (book.SeriesLink is null)
|
||||
return "";
|
||||
|
||||
max = Math.Max(max, 1);
|
||||
// first: alphabetical by name
|
||||
var withNames = book.SeriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(getSeriesNameString)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// then un-named are alpha by series id
|
||||
var nullNames = book.SeriesLink
|
||||
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(s => s.Series.AudibleSeriesId)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
|
||||
var titles = libraryBooks.Select(lb => "- " + lb.Book.TitleWithSubtitle).ToList();
|
||||
var titlesAgg = titles.Take(max).Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
if (titles.Count == max + 1)
|
||||
titlesAgg += $"\r\n\r\nand 1 other";
|
||||
else if (titles.Count > max + 1)
|
||||
titlesAgg += $"\r\n\r\nand {titles.Count - max } others";
|
||||
return titlesAgg;
|
||||
var all = withNames.Union(nullNames).ToList();
|
||||
return string.Join(", ", all);
|
||||
|
||||
string getSeriesNameString(SeriesBook sb)
|
||||
=> includeIndex && !string.IsNullOrWhiteSpace(sb.Order) && sb.Order != "-1"
|
||||
? $"{sb.Series.Name} (#{sb.Order})"
|
||||
: sb.Series.Name ?? "";
|
||||
}
|
||||
|
||||
public static float FirstScore(this Rating rating)
|
||||
=> rating.OverallRating > 0 ? rating.OverallRating
|
||||
: rating.PerformanceRating > 0 ? rating.PerformanceRating
|
||||
: rating.StoryRating;
|
||||
public static string ToStarString(this Rating rating)
|
||||
{
|
||||
var items = new List<string>();
|
||||
public string[] LowestCategoryNames()
|
||||
=> book.CategoriesLink?.Count is 0 or null ? []
|
||||
: book
|
||||
.CategoriesLink
|
||||
.Select(cl => cl.CategoryLadder.Categories.LastOrDefault()?.Name)
|
||||
.OfType<string>()
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (rating.OverallRating > 0)
|
||||
items.Add($"Overall: {getStars(rating.OverallRating)}");
|
||||
if (rating.PerformanceRating > 0)
|
||||
items.Add($"Perform: {getStars(rating.PerformanceRating)}");
|
||||
if (rating.StoryRating > 0)
|
||||
items.Add($"Story: {getStars(rating.StoryRating)}");
|
||||
public string[] AllCategoryNames()
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.Name)
|
||||
.ToArray();
|
||||
|
||||
return string.Join("\r\n", items);
|
||||
}
|
||||
/// <summary>character: ★</summary>
|
||||
const char STAR = '\u2605';
|
||||
/// <summary>character: ½</summary>
|
||||
const char HALF = '\u00BD';
|
||||
private static string getStars(float score)
|
||||
{
|
||||
var fullStars = (int)Math.Floor(score);
|
||||
public string[]? AllCategoryIds()
|
||||
=> book.CategoriesLink?.Count is null or 0 ? null
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.AudibleCategoryId)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var starString = new string(STAR, fullStars);
|
||||
|
||||
if (score - fullStars >= 0.75f)
|
||||
starString += STAR;
|
||||
else if (score - fullStars >= 0.25f)
|
||||
starString += HALF;
|
||||
public static string AggregateTitles(this IEnumerable<LibraryBook> libraryBooks, int max = 5)
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return "";
|
||||
|
||||
return starString;
|
||||
}
|
||||
}
|
||||
max = Math.Max(max, 1);
|
||||
|
||||
var titles = libraryBooks.Select(lb => "- " + lb.Book.TitleWithSubtitle).ToList();
|
||||
var titlesAgg = titles.Take(max).Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
if (titles.Count == max + 1)
|
||||
titlesAgg += $"\r\n\r\nand 1 other";
|
||||
else if (titles.Count > max + 1)
|
||||
titlesAgg += $"\r\n\r\nand {titles.Count - max} others";
|
||||
return titlesAgg;
|
||||
}
|
||||
|
||||
public static float FirstScore(this Rating rating)
|
||||
=> rating.OverallRating > 0 ? rating.OverallRating
|
||||
: rating.PerformanceRating > 0 ? rating.PerformanceRating
|
||||
: rating.StoryRating;
|
||||
public static string ToStarString(this Rating rating)
|
||||
{
|
||||
var items = new List<string>();
|
||||
|
||||
if (rating.OverallRating > 0)
|
||||
items.Add($"Overall: {getStars(rating.OverallRating)}");
|
||||
if (rating.PerformanceRating > 0)
|
||||
items.Add($"Perform: {getStars(rating.PerformanceRating)}");
|
||||
if (rating.StoryRating > 0)
|
||||
items.Add($"Story: {getStars(rating.StoryRating)}");
|
||||
|
||||
return string.Join("\r\n", items);
|
||||
}
|
||||
/// <summary>character: ★</summary>
|
||||
const char STAR = '\u2605';
|
||||
/// <summary>character: ½</summary>
|
||||
const char HALF = '\u00BD';
|
||||
private static string getStars(float score)
|
||||
{
|
||||
var fullStars = (int)Math.Floor(score);
|
||||
|
||||
var starString = new string(STAR, fullStars);
|
||||
|
||||
if (score - fullStars >= 0.75f)
|
||||
starString += STAR;
|
||||
else if (score - fullStars >= 0.25f)
|
||||
starString += HALF;
|
||||
|
||||
return starString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
internal class Formatters
|
||||
{
|
||||
internal class Formatters
|
||||
private static string[] _sortPrefixIgnores { get; } = { "the", "a", "an" };
|
||||
|
||||
public static string GetSortName(string unformattedName)
|
||||
{
|
||||
private static string[] _sortPrefixIgnores { get; } = { "the", "a", "an" };
|
||||
var sortName = unformattedName
|
||||
.Replace("|", "")
|
||||
.Replace(":", "")
|
||||
.ToLowerInvariant()
|
||||
.Trim();
|
||||
|
||||
public static string GetSortName(string unformattedName)
|
||||
{
|
||||
var sortName = unformattedName
|
||||
.Replace("|", "")
|
||||
.Replace(":", "")
|
||||
.ToLowerInvariant()
|
||||
.Trim();
|
||||
if (_sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " ")))
|
||||
sortName = sortName
|
||||
.Substring(sortName.IndexOf(" ") + 1)
|
||||
.TrimStart();
|
||||
|
||||
if (_sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " ")))
|
||||
sortName = sortName
|
||||
.Substring(sortName.IndexOf(" ") + 1)
|
||||
.TrimStart();
|
||||
|
||||
return sortName;
|
||||
}
|
||||
return sortName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace DataLayer;
|
||||
public interface INotifyDisposed : IDisposable
|
||||
{
|
||||
/// <summary> Event raised when the object is disposed. </summary>
|
||||
event EventHandler? ObjectDisposed;
|
||||
event EventHandler? ObjectDisposed;
|
||||
}
|
||||
|
||||
/// <summary> Creates a single instance of <typeparamref name="TDisposable"/> at a time, blocking subsequent creations until the previous creations are disposed. </summary>
|
||||
|
||||
@@ -3,62 +3,61 @@ using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class LibationContext : DbContext, INotifyDisposed
|
||||
{
|
||||
public class LibationContext : DbContext, INotifyDisposed
|
||||
{
|
||||
// IMPORTANT: USING DbSet<>
|
||||
// ========================
|
||||
// these run against the db. linq queries against these MUST be translatable to sql. primatives only. no POCOs or this error occurs:
|
||||
// Unable to create a constant value of type 'DataLayer.Contributor'. Only primitive types or enumeration types are supported in this context.
|
||||
// to use full object-linq, load and use Local. HOWEVER, Local is only hashed/indexed on PK. All other searches are very slow
|
||||
// load full table:
|
||||
// List<Contributor> contributors = ...;
|
||||
// Contributors.Load();
|
||||
// Contributors.Local.Where(a => contributors.Contains(a));
|
||||
// load only those in object:
|
||||
// // overwrite collection
|
||||
// Entry(product).Collection(x => x.Narrators).Load();
|
||||
// product.Narrators = narrators;
|
||||
public DbSet<LibraryBook> LibraryBooks { get; private set; }
|
||||
public DbSet<Book> Books { get; private set; }
|
||||
public DbSet<Contributor> Contributors { get; private set; }
|
||||
public DbSet<Series> Series { get; private set; }
|
||||
public DbSet<Category> Categories { get; private set; }
|
||||
public DbSet<CategoryLadder> CategoryLadders { get; private set; }
|
||||
// IMPORTANT: USING DbSet<>
|
||||
// ========================
|
||||
// these run against the db. linq queries against these MUST be translatable to sql. primatives only. no POCOs or this error occurs:
|
||||
// Unable to create a constant value of type 'DataLayer.Contributor'. Only primitive types or enumeration types are supported in this context.
|
||||
// to use full object-linq, load and use Local. HOWEVER, Local is only hashed/indexed on PK. All other searches are very slow
|
||||
// load full table:
|
||||
// List<Contributor> contributors = ...;
|
||||
// Contributors.Load();
|
||||
// Contributors.Local.Where(a => contributors.Contains(a));
|
||||
// load only those in object:
|
||||
// // overwrite collection
|
||||
// Entry(product).Collection(x => x.Narrators).Load();
|
||||
// product.Narrators = narrators;
|
||||
public DbSet<LibraryBook> LibraryBooks { get; private set; }
|
||||
public DbSet<Book> Books { get; private set; }
|
||||
public DbSet<Contributor> Contributors { get; private set; }
|
||||
public DbSet<Series> Series { get; private set; }
|
||||
public DbSet<Category> Categories { get; private set; }
|
||||
public DbSet<CategoryLadder> CategoryLadders { get; private set; }
|
||||
|
||||
public event EventHandler? ObjectDisposed;
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
ObjectDisposed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
ObjectDisposed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
public event EventHandler? ObjectDisposed;
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
ObjectDisposed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
ObjectDisposed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
|
||||
public LibationContext(DbContextOptions options) : base(options) { }
|
||||
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
|
||||
public LibationContext(DbContextOptions options) : base(options) { }
|
||||
|
||||
// typically only called once per execution; NOT once per instantiation
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
// typically only called once per execution; NOT once per instantiation
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.ApplyConfiguration(new BookConfig());
|
||||
modelBuilder.ApplyConfiguration(new ContributorConfig());
|
||||
modelBuilder.ApplyConfiguration(new BookContributorConfig());
|
||||
modelBuilder.ApplyConfiguration(new LibraryBookConfig());
|
||||
modelBuilder.ApplyConfiguration(new SeriesConfig());
|
||||
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
|
||||
modelBuilder.ApplyConfiguration(new CategoryConfig());
|
||||
modelBuilder.ApplyConfiguration(new CategoryLadderConfig());
|
||||
modelBuilder.ApplyConfiguration(new BookCategoryConfig());
|
||||
modelBuilder.ApplyConfiguration(new BookConfig());
|
||||
modelBuilder.ApplyConfiguration(new ContributorConfig());
|
||||
modelBuilder.ApplyConfiguration(new BookContributorConfig());
|
||||
modelBuilder.ApplyConfiguration(new LibraryBookConfig());
|
||||
modelBuilder.ApplyConfiguration(new SeriesConfig());
|
||||
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
|
||||
modelBuilder.ApplyConfiguration(new CategoryConfig());
|
||||
modelBuilder.ApplyConfiguration(new CategoryLadderConfig());
|
||||
modelBuilder.ApplyConfiguration(new BookCategoryConfig());
|
||||
|
||||
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
|
||||
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types
|
||||
}
|
||||
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
|
||||
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,39 +3,38 @@ using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
|
||||
using System;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public class LibationContextFactory
|
||||
{
|
||||
public class LibationContextFactory
|
||||
{
|
||||
public static void ConfigureOptions(NpgsqlDbContextOptionsBuilder options)
|
||||
{
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
options.MigrationsAssembly("DataLayer.Postgres");
|
||||
options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||
}
|
||||
public static void ConfigureOptions(NpgsqlDbContextOptionsBuilder options)
|
||||
{
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
options.MigrationsAssembly("DataLayer.Postgres");
|
||||
options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||
}
|
||||
|
||||
public static LibationContext CreatePostgres(string connectionString)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<LibationContext>();
|
||||
public static LibationContext CreatePostgres(string connectionString)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<LibationContext>();
|
||||
|
||||
options.UseNpgsql(connectionString, ConfigureOptions);
|
||||
options.UseNpgsql(connectionString, ConfigureOptions);
|
||||
|
||||
return new LibationContext(options.Options);
|
||||
}
|
||||
return new LibationContext(options.Options);
|
||||
}
|
||||
|
||||
public static LibationContext CreateSqlite(string connectionString)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<LibationContext>();
|
||||
public static LibationContext CreateSqlite(string connectionString)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<LibationContext>();
|
||||
|
||||
options
|
||||
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
|
||||
.UseSqlite(connectionString, options =>
|
||||
{
|
||||
options.MigrationsAssembly("DataLayer.Sqlite");
|
||||
options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||
});
|
||||
options
|
||||
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
|
||||
.UseSqlite(connectionString, options =>
|
||||
{
|
||||
options.MigrationsAssembly("DataLayer.Sqlite");
|
||||
options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||
});
|
||||
|
||||
return new LibationContext(options.Options);
|
||||
}
|
||||
}
|
||||
return new LibationContext(options.Options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace DataLayer;
|
||||
|
||||
public class MockLibraryBook : LibraryBook
|
||||
{
|
||||
protected MockLibraryBook(Book book, DateTime dateAdded, string account, DateTime? includedUntil, bool isAudiblePlus)
|
||||
@@ -123,6 +124,6 @@ public class MockLibraryBook : LibraryBook
|
||||
};
|
||||
}
|
||||
|
||||
private static string CalculateAsin(string name)
|
||||
private static string CalculateAsin(string name)
|
||||
=> Convert.ToHexString(System.Security.Cryptography.MD5.HashData(Encoding.UTF8.GetBytes(name))).Substring(0, 10);
|
||||
}
|
||||
|
||||
@@ -1,55 +1,54 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
// only library importing should use tracking. All else should be NoTracking.
|
||||
// only library importing should directly query Book. All else should use LibraryBook
|
||||
public static class BookQueries
|
||||
{
|
||||
// only library importing should use tracking. All else should be NoTracking.
|
||||
// only library importing should directly query Book. All else should use LibraryBook
|
||||
public static class BookQueries
|
||||
{
|
||||
public static Book? GetBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||
=> context
|
||||
.Books
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.GetBook(productId);
|
||||
public static Book? GetBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||
=> context
|
||||
.Books
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.GetBook(productId);
|
||||
|
||||
public static Book? GetBook(this IQueryable<Book> books, string productId)
|
||||
=> books
|
||||
.GetBooks()
|
||||
// 'Single' is more accurate but 'First' is faster and less error prone
|
||||
.FirstOrDefault(b => b.AudibleProductId == productId);
|
||||
public static Book? GetBook(this IQueryable<Book> books, string productId)
|
||||
=> books
|
||||
.GetBooks()
|
||||
// 'Single' is more accurate but 'First' is faster and less error prone
|
||||
.FirstOrDefault(b => b.AudibleProductId == productId);
|
||||
|
||||
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||
public static IQueryable<Book> GetBooks(this IQueryable<Book> books, Expression<Func<Book, bool>> predicate)
|
||||
=> books
|
||||
.GetBooks()
|
||||
.Where(predicate);
|
||||
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||
public static IQueryable<Book> GetBooks(this IQueryable<Book> books, Expression<Func<Book, bool>> predicate)
|
||||
=> books
|
||||
.GetBooks()
|
||||
.Where(predicate);
|
||||
|
||||
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||
public static IQueryable<Book> GetBooks(this IQueryable<Book> books)
|
||||
=> books
|
||||
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
|
||||
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
||||
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
||||
.Include(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories);
|
||||
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||
public static IQueryable<Book> GetBooks(this IQueryable<Book> books)
|
||||
=> books
|
||||
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
|
||||
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
||||
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
||||
.Include(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories);
|
||||
|
||||
public static bool IsProduct(this Book book)
|
||||
=> book.ContentType is not ContentType.Episode and not ContentType.Parent;
|
||||
public static bool IsProduct(this Book book)
|
||||
=> book.ContentType is not ContentType.Episode and not ContentType.Parent;
|
||||
|
||||
public static bool IsEpisodeChild(this Book book)
|
||||
=> book.ContentType is ContentType.Episode;
|
||||
public static bool IsEpisodeChild(this Book book)
|
||||
=> book.ContentType is ContentType.Episode;
|
||||
|
||||
public static bool IsEpisodeParent(this Book book)
|
||||
=> book.ContentType is ContentType.Parent;
|
||||
public static bool IsEpisodeParent(this Book book)
|
||||
=> book.ContentType is ContentType.Parent;
|
||||
|
||||
public static IEnumerable<LibraryBook> WithoutParents(this IEnumerable<LibraryBook> libraryBooks)
|
||||
=> libraryBooks.Where(lb => !lb.Book.IsEpisodeParent());
|
||||
public static IEnumerable<LibraryBook> WithoutParents(this IEnumerable<LibraryBook> libraryBooks)
|
||||
=> libraryBooks.Where(lb => !lb.Book.IsEpisodeParent());
|
||||
|
||||
public static bool HasLiberated(this Book book)
|
||||
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
|
||||
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
|
||||
}
|
||||
public static bool HasLiberated(this Book book)
|
||||
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
|
||||
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer
|
||||
namespace DataLayer;
|
||||
|
||||
public static class CategoryQueries
|
||||
{
|
||||
public static class CategoryQueries
|
||||
{
|
||||
public static IQueryable<CategoryLadder> GetCategoryLadders(this LibationContext context)
|
||||
=> context.CategoryLadders.Include(c => c._categories);
|
||||
}
|
||||
public static IQueryable<CategoryLadder> GetCategoryLadders(this LibationContext context)
|
||||
=> context.CategoryLadders.Include(c => c._categories);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer;
|
||||
|
||||
|
||||
@@ -1,252 +1,251 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DtoImporterService
|
||||
namespace DtoImporterService;
|
||||
|
||||
public class BookImporter : ItemsImporterBase
|
||||
{
|
||||
public class BookImporter : ItemsImporterBase
|
||||
protected override IValidator Validator => new BookValidator();
|
||||
|
||||
public Dictionary<string, Book> Cache { get; private set; } = new();
|
||||
|
||||
private ContributorImporter contributorImporter { get; }
|
||||
private SeriesImporter seriesImporter { get; }
|
||||
private CategoryImporter categoryImporter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether <see cref="BookImporter"/> loaded every Book from the <seealso cref="LibationContext"/> during import.
|
||||
/// If true, the DbContext was queried for all Books, rather than just those being imported.
|
||||
/// If means that all <see cref="LibraryBook"/> objects in the DbContext will have their <see cref="LibraryBook.Book"/> property populated.
|
||||
/// If false, only those Books being imported were loaded, and some <see cref="LibraryBook"/> objects will have a null <see cref="LibraryBook.Book"/> property for books not included in the import set.
|
||||
/// </summary>
|
||||
internal bool LoadedEntireLibrary { get; private set; }
|
||||
|
||||
public BookImporter(LibationContext context) : base(context)
|
||||
{
|
||||
protected override IValidator Validator => new BookValidator();
|
||||
|
||||
public Dictionary<string, Book> Cache { get; private set; } = new();
|
||||
|
||||
private ContributorImporter contributorImporter { get; }
|
||||
private SeriesImporter seriesImporter { get; }
|
||||
private CategoryImporter categoryImporter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether <see cref="BookImporter"/> loaded every Book from the <seealso cref="LibationContext"/> during import.
|
||||
/// If true, the DbContext was queried for all Books, rather than just those being imported.
|
||||
/// If means that all <see cref="LibraryBook"/> objects in the DbContext will have their <see cref="LibraryBook.Book"/> property populated.
|
||||
/// If false, only those Books being imported were loaded, and some <see cref="LibraryBook"/> objects will have a null <see cref="LibraryBook.Book"/> property for books not included in the import set.
|
||||
/// </summary>
|
||||
internal bool LoadedEntireLibrary { get; private set; }
|
||||
|
||||
public BookImporter(LibationContext context) : base(context)
|
||||
{
|
||||
contributorImporter = new ContributorImporter(DbContext);
|
||||
seriesImporter = new SeriesImporter(DbContext);
|
||||
categoryImporter = new CategoryImporter(DbContext);
|
||||
}
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// pre-req.s
|
||||
contributorImporter.Import(importItems);
|
||||
seriesImporter.Import(importItems);
|
||||
categoryImporter.Import(importItems);
|
||||
|
||||
// load db existing => hash table
|
||||
loadLocal_books(importItems);
|
||||
|
||||
// upsert
|
||||
var qtyNew = upsertBooks(importItems);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private void loadLocal_books(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// get distinct
|
||||
var productIds = importItems
|
||||
.Select(i => i.DtoItem.ProductId)
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
|
||||
if (productIds.Count > 100)
|
||||
{
|
||||
//For large imports, it is faster to get the whole library and filter in memory.
|
||||
Cache = DbContext.Books
|
||||
.GetBooks()
|
||||
.ToArray()
|
||||
.Where(b => productIds.Contains(b.AudibleProductId))
|
||||
.ToDictionarySafe(b => b.AudibleProductId);
|
||||
LoadedEntireLibrary = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Cache = DbContext.Books
|
||||
.GetBooks(b => productIds.Contains(b.AudibleProductId))
|
||||
.ToDictionarySafe(b => b.AudibleProductId);
|
||||
}
|
||||
}
|
||||
|
||||
private int upsertBooks(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var item in importItems)
|
||||
{
|
||||
if (item.DtoItem.ProductId is null)
|
||||
continue;
|
||||
if (!Cache.TryGetValue(item.DtoItem.ProductId, out var book))
|
||||
{
|
||||
book = createNewBook(item);
|
||||
qtyNew++;
|
||||
}
|
||||
|
||||
updateBook(item, book);
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private Book createNewBook(ImportItem importItem)
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
var contentType = GetContentType(item);
|
||||
|
||||
// absence of authors is very rare, but possible
|
||||
if (item.Authors?.Length is null or 0)
|
||||
item.Authors = [new Person { Name = "", Asin = null }];
|
||||
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
var authors = ContributorsFromCache(item.Authors);
|
||||
|
||||
var narrators
|
||||
= item.Narrators?.Length is null or 0
|
||||
// if no narrators listed, author is the narrator
|
||||
? authors
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
: ContributorsFromCache(item.Narrators);
|
||||
|
||||
Book book;
|
||||
try
|
||||
{
|
||||
if (item.ProductId is null)
|
||||
throw new ArgumentNullException(nameof(item.ProductId), "ProductId is null when trying to create new Book.");
|
||||
|
||||
book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.Title,
|
||||
item.Subtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
authors,
|
||||
narrators,
|
||||
importItem.LocaleName
|
||||
)
|
||||
).Entity;
|
||||
Cache.Add(book.AudibleProductId, book);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding book. {@DebugInfo}", new
|
||||
{
|
||||
item.ProductId,
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
QtyAuthors = authors?.Length,
|
||||
QtyNarrators = narrators?.Length,
|
||||
importItem.LocaleName
|
||||
});
|
||||
throw;
|
||||
}
|
||||
|
||||
var publisherName = item.Publisher;
|
||||
if (!string.IsNullOrWhiteSpace(publisherName))
|
||||
{
|
||||
var publisher = contributorImporter.Cache[publisherName];
|
||||
book.ReplacePublisher(publisher);
|
||||
}
|
||||
|
||||
if (item.PdfUrl is not null)
|
||||
book.AddSupplementDownloadUrl(item.PdfUrl.ToString());
|
||||
|
||||
return book;
|
||||
}
|
||||
|
||||
private void updateBook(ImportItem importItem, Book book)
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
// Replacing narrators only became necessary to correct a bug introduced in 13.1.0
|
||||
// which would no import narrators with null ASINs. Thus, affected books had the
|
||||
// author listed as the narrators. This can probably be removed in the future.
|
||||
// Bug went live in 13.1.0 on 2026/01/02. Today is 2026/01/08.
|
||||
if (ContributorsFromCache(item.Narrators) is { } narrators && narrators.Length > 0)
|
||||
book.ReplaceNarrators(narrators);
|
||||
|
||||
book.UpdateLengthInMinutes(item.LengthInMinutes);
|
||||
|
||||
// Update the book titles, since formatting can change
|
||||
book.UpdateTitle(item.Title, item.Subtitle);
|
||||
|
||||
// set/update book-specific info which may have changed
|
||||
if (item.PictureId is not null)
|
||||
book.PictureId = item.PictureId;
|
||||
|
||||
if (item.PictureLarge is not null)
|
||||
book.PictureLarge = item.PictureLarge;
|
||||
|
||||
if (item.IsFinished is not null)
|
||||
book.UserDefinedItem.IsFinished = item.IsFinished.Value;
|
||||
|
||||
// 2023-02-01
|
||||
// updateBook must update language on books which were imported before the migration which added language.
|
||||
// 2025-07-30
|
||||
// updateBook must update isSpatial on books which were imported before the migration which added isSpatial.
|
||||
book.UpdateBookDetails(item.IsAbridged, item.AssetDetails?.Any(a => a.IsSpatial), item.DatePublished, item.Language);
|
||||
|
||||
book.UpdateProductRating(
|
||||
(float)(item.Rating?.OverallDistribution?.AverageRating ?? 0),
|
||||
(float)(item.Rating?.PerformanceDistribution?.AverageRating ?? 0),
|
||||
(float)(item.Rating?.StoryDistribution?.AverageRating ?? 0));
|
||||
|
||||
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
|
||||
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
|
||||
|
||||
// update series even for existing books. these are occasionally updated
|
||||
// these will upsert over library-scraped series, but will not leave orphans
|
||||
if (item.Series is not null)
|
||||
{
|
||||
foreach (var seriesEntry in item.Series)
|
||||
{
|
||||
if (string.IsNullOrEmpty(seriesEntry.SeriesId))
|
||||
continue;
|
||||
var series = seriesImporter.Cache[seriesEntry.SeriesId];
|
||||
book.UpsertSeries(series, seriesEntry.Sequence);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.CategoryLadders is not null)
|
||||
{
|
||||
var ladders = new List<DataLayer.CategoryLadder>();
|
||||
foreach (var ladder in item.CategoryLadders.Select(cl => cl?.Ladder).Where(l => l?.Length > 0))
|
||||
{
|
||||
var categoryIds = ladder?.Select(l => l?.CategoryId).ToList();
|
||||
ladders.Add(categoryImporter.LadderCache.Single(c => c.Equals(categoryIds)));
|
||||
}
|
||||
//Set all ladders at once so ladders that have been
|
||||
//removed by audible can be removed from the DB
|
||||
book.SetCategoryLadders(ladders);
|
||||
}
|
||||
}
|
||||
|
||||
private static DataLayer.ContentType GetContentType(Item item)
|
||||
{
|
||||
if (item.IsEpisodes)
|
||||
return DataLayer.ContentType.Episode;
|
||||
else if (item.IsSeriesParent)
|
||||
return DataLayer.ContentType.Parent;
|
||||
else
|
||||
return DataLayer.ContentType.Product;
|
||||
}
|
||||
|
||||
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(toLoad))]
|
||||
private Contributor[]? ContributorsFromCache(IEnumerable<Person>? toLoad)
|
||||
=> toLoad
|
||||
?.Select(a => a.Name)
|
||||
.OfType<string>()
|
||||
.Distinct()
|
||||
.Select(name => contributorImporter.Cache[name])
|
||||
.ToArray();
|
||||
contributorImporter = new ContributorImporter(DbContext);
|
||||
seriesImporter = new SeriesImporter(DbContext);
|
||||
categoryImporter = new CategoryImporter(DbContext);
|
||||
}
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// pre-req.s
|
||||
contributorImporter.Import(importItems);
|
||||
seriesImporter.Import(importItems);
|
||||
categoryImporter.Import(importItems);
|
||||
|
||||
// load db existing => hash table
|
||||
loadLocal_books(importItems);
|
||||
|
||||
// upsert
|
||||
var qtyNew = upsertBooks(importItems);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private void loadLocal_books(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// get distinct
|
||||
var productIds = importItems
|
||||
.Select(i => i.DtoItem.ProductId)
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
|
||||
if (productIds.Count > 100)
|
||||
{
|
||||
//For large imports, it is faster to get the whole library and filter in memory.
|
||||
Cache = DbContext.Books
|
||||
.GetBooks()
|
||||
.ToArray()
|
||||
.Where(b => productIds.Contains(b.AudibleProductId))
|
||||
.ToDictionarySafe(b => b.AudibleProductId);
|
||||
LoadedEntireLibrary = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Cache = DbContext.Books
|
||||
.GetBooks(b => productIds.Contains(b.AudibleProductId))
|
||||
.ToDictionarySafe(b => b.AudibleProductId);
|
||||
}
|
||||
}
|
||||
|
||||
private int upsertBooks(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var item in importItems)
|
||||
{
|
||||
if (item.DtoItem.ProductId is null)
|
||||
continue;
|
||||
if (!Cache.TryGetValue(item.DtoItem.ProductId, out var book))
|
||||
{
|
||||
book = createNewBook(item);
|
||||
qtyNew++;
|
||||
}
|
||||
|
||||
updateBook(item, book);
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private Book createNewBook(ImportItem importItem)
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
var contentType = GetContentType(item);
|
||||
|
||||
// absence of authors is very rare, but possible
|
||||
if (item.Authors?.Length is null or 0)
|
||||
item.Authors = [new Person { Name = "", Asin = null }];
|
||||
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
var authors = ContributorsFromCache(item.Authors);
|
||||
|
||||
var narrators
|
||||
= item.Narrators?.Length is null or 0
|
||||
// if no narrators listed, author is the narrator
|
||||
? authors
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
: ContributorsFromCache(item.Narrators);
|
||||
|
||||
Book book;
|
||||
try
|
||||
{
|
||||
if (item.ProductId is null)
|
||||
throw new ArgumentNullException(nameof(item.ProductId), "ProductId is null when trying to create new Book.");
|
||||
|
||||
book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.Title,
|
||||
item.Subtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
authors,
|
||||
narrators,
|
||||
importItem.LocaleName
|
||||
)
|
||||
).Entity;
|
||||
Cache.Add(book.AudibleProductId, book);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding book. {@DebugInfo}", new
|
||||
{
|
||||
item.ProductId,
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
QtyAuthors = authors?.Length,
|
||||
QtyNarrators = narrators?.Length,
|
||||
importItem.LocaleName
|
||||
});
|
||||
throw;
|
||||
}
|
||||
|
||||
var publisherName = item.Publisher;
|
||||
if (!string.IsNullOrWhiteSpace(publisherName))
|
||||
{
|
||||
var publisher = contributorImporter.Cache[publisherName];
|
||||
book.ReplacePublisher(publisher);
|
||||
}
|
||||
|
||||
if (item.PdfUrl is not null)
|
||||
book.AddSupplementDownloadUrl(item.PdfUrl.ToString());
|
||||
|
||||
return book;
|
||||
}
|
||||
|
||||
private void updateBook(ImportItem importItem, Book book)
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
// Replacing narrators only became necessary to correct a bug introduced in 13.1.0
|
||||
// which would no import narrators with null ASINs. Thus, affected books had the
|
||||
// author listed as the narrators. This can probably be removed in the future.
|
||||
// Bug went live in 13.1.0 on 2026/01/02. Today is 2026/01/08.
|
||||
if (ContributorsFromCache(item.Narrators) is { } narrators && narrators.Length > 0)
|
||||
book.ReplaceNarrators(narrators);
|
||||
|
||||
book.UpdateLengthInMinutes(item.LengthInMinutes);
|
||||
|
||||
// Update the book titles, since formatting can change
|
||||
book.UpdateTitle(item.Title, item.Subtitle);
|
||||
|
||||
// set/update book-specific info which may have changed
|
||||
if (item.PictureId is not null)
|
||||
book.PictureId = item.PictureId;
|
||||
|
||||
if (item.PictureLarge is not null)
|
||||
book.PictureLarge = item.PictureLarge;
|
||||
|
||||
if (item.IsFinished is not null)
|
||||
book.UserDefinedItem.IsFinished = item.IsFinished.Value;
|
||||
|
||||
// 2023-02-01
|
||||
// updateBook must update language on books which were imported before the migration which added language.
|
||||
// 2025-07-30
|
||||
// updateBook must update isSpatial on books which were imported before the migration which added isSpatial.
|
||||
book.UpdateBookDetails(item.IsAbridged, item.AssetDetails?.Any(a => a.IsSpatial), item.DatePublished, item.Language);
|
||||
|
||||
book.UpdateProductRating(
|
||||
(float)(item.Rating?.OverallDistribution?.AverageRating ?? 0),
|
||||
(float)(item.Rating?.PerformanceDistribution?.AverageRating ?? 0),
|
||||
(float)(item.Rating?.StoryDistribution?.AverageRating ?? 0));
|
||||
|
||||
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
|
||||
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
|
||||
|
||||
// update series even for existing books. these are occasionally updated
|
||||
// these will upsert over library-scraped series, but will not leave orphans
|
||||
if (item.Series is not null)
|
||||
{
|
||||
foreach (var seriesEntry in item.Series)
|
||||
{
|
||||
if (string.IsNullOrEmpty(seriesEntry.SeriesId))
|
||||
continue;
|
||||
var series = seriesImporter.Cache[seriesEntry.SeriesId];
|
||||
book.UpsertSeries(series, seriesEntry.Sequence);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.CategoryLadders is not null)
|
||||
{
|
||||
var ladders = new List<DataLayer.CategoryLadder>();
|
||||
foreach (var ladder in item.CategoryLadders.Select(cl => cl?.Ladder).Where(l => l?.Length > 0))
|
||||
{
|
||||
var categoryIds = ladder?.Select(l => l?.CategoryId).ToList();
|
||||
ladders.Add(categoryImporter.LadderCache.Single(c => c.Equals(categoryIds)));
|
||||
}
|
||||
//Set all ladders at once so ladders that have been
|
||||
//removed by audible can be removed from the DB
|
||||
book.SetCategoryLadders(ladders);
|
||||
}
|
||||
}
|
||||
|
||||
private static DataLayer.ContentType GetContentType(Item item)
|
||||
{
|
||||
if (item.IsEpisodes)
|
||||
return DataLayer.ContentType.Episode;
|
||||
else if (item.IsSeriesParent)
|
||||
return DataLayer.ContentType.Parent;
|
||||
else
|
||||
return DataLayer.ContentType.Product;
|
||||
}
|
||||
|
||||
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(toLoad))]
|
||||
private Contributor[]? ContributorsFromCache(IEnumerable<Person>? toLoad)
|
||||
=> toLoad
|
||||
?.Select(a => a.Name)
|
||||
.OfType<string>()
|
||||
.Distinct()
|
||||
.Select(name => contributorImporter.Cache[name])
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
@@ -1,117 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DtoImporterService
|
||||
namespace DtoImporterService;
|
||||
|
||||
public class CategoryImporter : ItemsImporterBase
|
||||
{
|
||||
public class CategoryImporter : ItemsImporterBase
|
||||
protected override IValidator Validator => new CategoryValidator();
|
||||
|
||||
private Dictionary<string, Category> CategoryCache { get; set; } = new();
|
||||
public HashSet<DataLayer.CategoryLadder> LadderCache { get; private set; } = new();
|
||||
|
||||
public CategoryImporter(LibationContext context) : base(context) { }
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
protected override IValidator Validator => new CategoryValidator();
|
||||
// load db existing => .Local
|
||||
loadLocal_categories();
|
||||
|
||||
private Dictionary<string, Category> CategoryCache { get; set; } = new();
|
||||
public HashSet<DataLayer.CategoryLadder> LadderCache { get; private set; } = new();
|
||||
// upsert
|
||||
//Import item may not have no (null) categories
|
||||
var categoryLadders = importItems
|
||||
.Where(i => i.DtoItem.CategoryLadders is not null)
|
||||
.SelectMany(i => i.DtoItem.CategoryLadders!)
|
||||
.Select(cl => cl?.Ladder)
|
||||
.Where(l => l?.Length > 0)
|
||||
.OfType<Ladder[]>()
|
||||
.ToList();
|
||||
|
||||
public CategoryImporter(LibationContext context) : base(context) { }
|
||||
var qtyNew = upsertCategories(categoryLadders);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
private void loadLocal_categories()
|
||||
{
|
||||
// load existing => local
|
||||
LadderCache = DbContext.GetCategoryLadders().ToHashSet();
|
||||
CategoryCache = LadderCache.SelectMany(cl => cl.Categories).ToDictionarySafe(c => c.AudibleCategoryId);
|
||||
}
|
||||
|
||||
// only use after loading contributors => local
|
||||
private int upsertCategories(List<Ladder[]> ladders)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var ladder in ladders)
|
||||
{
|
||||
// load db existing => .Local
|
||||
loadLocal_categories();
|
||||
var categories = new List<Category>(ladder.Length);
|
||||
|
||||
// upsert
|
||||
//Import item may not have no (null) categories
|
||||
var categoryLadders = importItems
|
||||
.Where(i => i.DtoItem.CategoryLadders is not null)
|
||||
.SelectMany(i => i.DtoItem.CategoryLadders!)
|
||||
.Select(cl => cl?.Ladder)
|
||||
.Where(l => l?.Length > 0)
|
||||
.OfType<Ladder[]>()
|
||||
.ToList();
|
||||
|
||||
var qtyNew = upsertCategories(categoryLadders);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private void loadLocal_categories()
|
||||
{
|
||||
// load existing => local
|
||||
LadderCache = DbContext.GetCategoryLadders().ToHashSet();
|
||||
CategoryCache = LadderCache.SelectMany(cl => cl.Categories).ToDictionarySafe(c => c.AudibleCategoryId);
|
||||
}
|
||||
|
||||
// only use after loading contributors => local
|
||||
private int upsertCategories(List<Ladder[]> ladders)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var ladder in ladders)
|
||||
for (var i = 0; i < ladder.Length; i++)
|
||||
{
|
||||
var categories = new List<Category>(ladder.Length);
|
||||
var id = ladder[i].CategoryId;
|
||||
var name = ladder[i].CategoryName;
|
||||
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(name))
|
||||
continue;
|
||||
|
||||
for (var i = 0; i < ladder.Length; i++)
|
||||
if (!CategoryCache.TryGetValue(id, out var category))
|
||||
{
|
||||
var id = ladder[i].CategoryId;
|
||||
var name = ladder[i].CategoryName;
|
||||
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(name))
|
||||
continue;
|
||||
|
||||
if (!CategoryCache.TryGetValue(id, out var category))
|
||||
{
|
||||
category = addCategory(id, name);
|
||||
}
|
||||
|
||||
categories.Add(category);
|
||||
category = addCategory(id, name);
|
||||
}
|
||||
|
||||
var categoryLadder = new DataLayer.CategoryLadder(categories);
|
||||
if (!LadderCache.Contains(categoryLadder))
|
||||
{
|
||||
addCategoryLadder(categoryLadder);
|
||||
qtyNew++;
|
||||
}
|
||||
categories.Add(category);
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private DataLayer.CategoryLadder addCategoryLadder(DataLayer.CategoryLadder categoryList)
|
||||
{
|
||||
try
|
||||
var categoryLadder = new DataLayer.CategoryLadder(categories);
|
||||
if (!LadderCache.Contains(categoryLadder))
|
||||
{
|
||||
var entityEntry = DbContext.CategoryLadders.Add(categoryList);
|
||||
var entity = entityEntry.Entity;
|
||||
|
||||
LadderCache.Add(entity);
|
||||
return entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding category ladder.");
|
||||
throw;
|
||||
addCategoryLadder(categoryLadder);
|
||||
qtyNew++;
|
||||
}
|
||||
}
|
||||
|
||||
private Category addCategory(string id, string name)
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private DataLayer.CategoryLadder addCategoryLadder(DataLayer.CategoryLadder categoryList)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var category = new Category(new AudibleCategoryId(id), name);
|
||||
var entityEntry = DbContext.CategoryLadders.Add(categoryList);
|
||||
var entity = entityEntry.Entity;
|
||||
|
||||
var entityEntry = DbContext.Categories.Add(category);
|
||||
var entity = entityEntry.Entity;
|
||||
LadderCache.Add(entity);
|
||||
return entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding category ladder.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
CategoryCache.Add(entity.AudibleCategoryId, entity);
|
||||
return entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding category. {@DebugInfo}", new { id, name });
|
||||
throw;
|
||||
}
|
||||
private Category addCategory(string id, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var category = new Category(new AudibleCategoryId(id), name);
|
||||
|
||||
var entityEntry = DbContext.Categories.Add(category);
|
||||
var entity = entityEntry.Entity;
|
||||
|
||||
CategoryCache.Add(entity.AudibleCategoryId, entity);
|
||||
return entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding category. {@DebugInfo}", new { id, name });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DtoImporterService
|
||||
namespace DtoImporterService;
|
||||
|
||||
public class ContributorImporter : ItemsImporterBase
|
||||
{
|
||||
public class ContributorImporter : ItemsImporterBase
|
||||
protected override IValidator Validator => new ClearValidator();
|
||||
|
||||
public Dictionary<string, Contributor> Cache { get; private set; } = new();
|
||||
|
||||
public ContributorImporter(LibationContext context) : base(context) { }
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
protected override IValidator Validator => new ClearValidator();
|
||||
// get distinct
|
||||
var authors = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetAuthorsDistinct()
|
||||
.ToList();
|
||||
var narrators = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetNarratorsDistinct()
|
||||
.ToList();
|
||||
var publishers = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetPublishersDistinct()
|
||||
.ToList();
|
||||
|
||||
public Dictionary<string, Contributor> Cache { get; private set; } = new();
|
||||
// load db existing => .Local
|
||||
var allNames = publishers
|
||||
.Union(authors.Select(n => n.Name))
|
||||
.Union(narrators.Select(n => n.Name))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Cast<string>()
|
||||
.ToList();
|
||||
loadLocal_contributors(allNames);
|
||||
|
||||
public ContributorImporter(LibationContext context) : base(context) { }
|
||||
// upsert
|
||||
var qtyNew = 0;
|
||||
qtyNew += upsertPeople(authors);
|
||||
qtyNew += upsertPeople(narrators);
|
||||
qtyNew += upsertPublishers(publishers);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
private void loadLocal_contributors(List<string> contributorNames)
|
||||
{
|
||||
// must include default/empty/missing
|
||||
contributorNames.Add(Contributor.GetEmpty().Name);
|
||||
|
||||
// load existing => local
|
||||
Cache = DbContext.Contributors
|
||||
.Where(c => contributorNames.Contains(c.Name))
|
||||
.ToDictionarySafe(c => c.Name);
|
||||
}
|
||||
|
||||
private int upsertPeople(List<Person> people)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
foreach (var person in people)
|
||||
{
|
||||
// get distinct
|
||||
var authors = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetAuthorsDistinct()
|
||||
.ToList();
|
||||
var narrators = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetNarratorsDistinct()
|
||||
.ToList();
|
||||
var publishers = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetPublishersDistinct()
|
||||
.ToList();
|
||||
|
||||
// load db existing => .Local
|
||||
var allNames = publishers
|
||||
.Union(authors.Select(n => n.Name))
|
||||
.Union(narrators.Select(n => n.Name))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Cast<string>()
|
||||
.ToList();
|
||||
loadLocal_contributors(allNames);
|
||||
|
||||
// upsert
|
||||
var qtyNew = 0;
|
||||
qtyNew += upsertPeople(authors);
|
||||
qtyNew += upsertPeople(narrators);
|
||||
qtyNew += upsertPublishers(publishers);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private void loadLocal_contributors(List<string> contributorNames)
|
||||
{
|
||||
// must include default/empty/missing
|
||||
contributorNames.Add(Contributor.GetEmpty().Name);
|
||||
|
||||
// load existing => local
|
||||
Cache = DbContext.Contributors
|
||||
.Where(c => contributorNames.Contains(c.Name))
|
||||
.ToDictionarySafe(c => c.Name);
|
||||
}
|
||||
|
||||
private int upsertPeople(List<Person> people)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
foreach (var person in people)
|
||||
if (person.Name is null)
|
||||
continue;
|
||||
if (!Cache.TryGetValue(person.Name, out var contributor))
|
||||
{
|
||||
if (person.Name is null)
|
||||
continue;
|
||||
if (!Cache.TryGetValue(person.Name, out var contributor))
|
||||
{
|
||||
contributor = createContributor(person.Name, person.Asin);
|
||||
qtyNew++;
|
||||
}
|
||||
|
||||
updateContributor(person, contributor);
|
||||
contributor = createContributor(person.Name, person.Asin);
|
||||
qtyNew++;
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
updateContributor(person, contributor);
|
||||
}
|
||||
|
||||
// only use after loading contributors => local
|
||||
private int upsertPublishers(List<string> publishers)
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
// only use after loading contributors => local
|
||||
private int upsertPublishers(List<string> publishers)
|
||||
{
|
||||
var hash = publishers
|
||||
// new publishers only
|
||||
.Where(p => !Cache.ContainsKey(p))
|
||||
// remove duplicates
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var pub in hash)
|
||||
createContributor(pub);
|
||||
|
||||
return hash.Count;
|
||||
}
|
||||
|
||||
private void updateContributor(Person person, Contributor contributor)
|
||||
{
|
||||
if (person.Asin != contributor.AudibleContributorId)
|
||||
contributor.SetAudibleContributorId(person.Asin);
|
||||
}
|
||||
|
||||
private Contributor createContributor(string name, string? id = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hash = publishers
|
||||
// new publishers only
|
||||
.Where(p => !Cache.ContainsKey(p))
|
||||
// remove duplicates
|
||||
.ToHashSet();
|
||||
var newContrib = new Contributor(name, id);
|
||||
|
||||
foreach (var pub in hash)
|
||||
createContributor(pub);
|
||||
var entityEntry = DbContext.Contributors.Add(newContrib);
|
||||
var entity = entityEntry.Entity;
|
||||
|
||||
return hash.Count;
|
||||
Cache.Add(entity.Name, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void updateContributor(Person person, Contributor contributor)
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (person.Asin != contributor.AudibleContributorId)
|
||||
contributor.SetAudibleContributorId(person.Asin);
|
||||
}
|
||||
|
||||
private Contributor createContributor(string name, string? id = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newContrib = new Contributor(name, id);
|
||||
|
||||
var entityEntry = DbContext.Contributors.Add(newContrib);
|
||||
var entity = entityEntry.Entity;
|
||||
|
||||
Cache.Add(entity.Name, entity);
|
||||
return entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding contributor. {@DebugInfo}", new { name, id });
|
||||
throw;
|
||||
}
|
||||
Serilog.Log.Logger.Error(ex, "Error adding contributor. {@DebugInfo}", new { name, id });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using AudibleApi.Common;
|
||||
using AudibleApi.Common;
|
||||
|
||||
namespace DtoImporterService
|
||||
namespace DtoImporterService;
|
||||
|
||||
public record ImportItem(Item DtoItem, string AccountId, string LocaleName)
|
||||
{
|
||||
public record ImportItem(Item DtoItem, string AccountId, string LocaleName)
|
||||
{
|
||||
public override string ToString() => $"[{DtoItem.ProductId}] {DtoItem.Title}";
|
||||
}
|
||||
public override string ToString() => $"[{DtoItem.ProductId}] {DtoItem.Title}";
|
||||
}
|
||||
|
||||
@@ -1,61 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleUtilities;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DtoImporterService
|
||||
namespace DtoImporterService;
|
||||
|
||||
public abstract class ImporterBase<T>
|
||||
{
|
||||
public abstract class ImporterBase<T>
|
||||
protected LibationContext DbContext { get; }
|
||||
|
||||
protected ImporterBase(LibationContext context)
|
||||
{
|
||||
protected LibationContext DbContext { get; }
|
||||
|
||||
protected ImporterBase(LibationContext context)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||
DbContext = context;
|
||||
}
|
||||
|
||||
/// <summary>LONG RUNNING. call with await Task.Run</summary>
|
||||
public int Import(T param) => Run(DoImport, param);
|
||||
|
||||
public TResult Run<TResult>(Func<T, TResult> func, T param)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exceptions = Validate(param);
|
||||
if (exceptions is not null && exceptions.Any())
|
||||
throw new AggregateException($"Importer validation failed", exceptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Import error: validation");
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = func(param);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Import error: post-validation importing");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract int DoImport(T elements);
|
||||
public abstract IEnumerable<Exception> Validate(T param);
|
||||
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||
DbContext = context;
|
||||
}
|
||||
|
||||
public abstract class ItemsImporterBase : ImporterBase<IEnumerable<ImportItem>>
|
||||
{
|
||||
protected ItemsImporterBase(LibationContext context) : base(context) { }
|
||||
/// <summary>LONG RUNNING. call with await Task.Run</summary>
|
||||
public int Import(T param) => Run(DoImport, param);
|
||||
|
||||
protected abstract IValidator Validator { get; }
|
||||
public sealed override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems)
|
||||
=> Validator.Validate(importItems.Select(i => i.DtoItem));
|
||||
public TResult Run<TResult>(Func<T, TResult> func, T param)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exceptions = Validate(param);
|
||||
if (exceptions is not null && exceptions.Any())
|
||||
throw new AggregateException($"Importer validation failed", exceptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Import error: validation");
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = func(param);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Import error: post-validation importing");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract int DoImport(T elements);
|
||||
public abstract IEnumerable<Exception> Validate(T param);
|
||||
}
|
||||
|
||||
public abstract class ItemsImporterBase : ImporterBase<IEnumerable<ImportItem>>
|
||||
{
|
||||
protected ItemsImporterBase(LibationContext context) : base(context) { }
|
||||
|
||||
protected abstract IValidator Validator { get; }
|
||||
public sealed override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems)
|
||||
=> Validator.Validate(importItems.Select(i => i.DtoItem));
|
||||
}
|
||||
|
||||
@@ -1,146 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleUtilities;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DtoImporterService;
|
||||
|
||||
public class LibraryBookImporter : ItemsImporterBase
|
||||
{
|
||||
protected override IValidator Validator => new LibraryValidator();
|
||||
protected override IValidator Validator => new LibraryValidator();
|
||||
|
||||
private BookImporter bookImporter { get; }
|
||||
private BookImporter bookImporter { get; }
|
||||
|
||||
public LibraryBookImporter(LibationContext context) : base(context)
|
||||
{
|
||||
bookImporter = new BookImporter(DbContext);
|
||||
}
|
||||
public LibraryBookImporter(LibationContext context) : base(context)
|
||||
{
|
||||
bookImporter = new BookImporter(DbContext);
|
||||
}
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
bookImporter.Import(importItems);
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
bookImporter.Import(importItems);
|
||||
|
||||
var qtyNew = upsertLibraryBooks(importItems);
|
||||
return qtyNew;
|
||||
}
|
||||
var qtyNew = upsertLibraryBooks(importItems);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private int upsertLibraryBooks(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// technically, we should be able to have duplicate books from separate accounts.
|
||||
// this would violate the current pk and would be difficult to deal with elsewhere:
|
||||
// - what to show in the grid
|
||||
// - which to consider liberated
|
||||
//
|
||||
// sqlite cannot alter pk. the work around is an extensive headache
|
||||
// - update: now possible in .net5/efcore5
|
||||
//
|
||||
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
private int upsertLibraryBooks(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// technically, we should be able to have duplicate books from separate accounts.
|
||||
// this would violate the current pk and would be difficult to deal with elsewhere:
|
||||
// - what to show in the grid
|
||||
// - which to consider liberated
|
||||
//
|
||||
// sqlite cannot alter pk. the work around is an extensive headache
|
||||
// - update: now possible in .net5/efcore5
|
||||
//
|
||||
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
|
||||
//When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext
|
||||
//instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter.
|
||||
//There should never be duplicates, but this is defensive.
|
||||
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId);
|
||||
//When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext
|
||||
//instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter.
|
||||
//There should never be duplicates, but this is defensive.
|
||||
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId);
|
||||
|
||||
//If importItems are contains duplicates by asin, keep the Item that's "available"
|
||||
var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId ?? string.Empty, tieBreak);
|
||||
//If importItems are contains duplicates by asin, keep the Item that's "available"
|
||||
var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId ?? string.Empty, tieBreak);
|
||||
|
||||
int qtyNew = 0;
|
||||
foreach (var item in uniqueImportItems.Values)
|
||||
{
|
||||
if (item.DtoItem.ProductId is null)
|
||||
continue;
|
||||
if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook? existing))
|
||||
{
|
||||
if (existing.Account != item.AccountId)
|
||||
{
|
||||
//Book is absent from the existing LibraryBook's account. Use the alternate account.
|
||||
existing.SetAccount(item.AccountId);
|
||||
}
|
||||
int qtyNew = 0;
|
||||
foreach (var item in uniqueImportItems.Values)
|
||||
{
|
||||
if (item.DtoItem.ProductId is null)
|
||||
continue;
|
||||
if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook? existing))
|
||||
{
|
||||
if (existing.Account != item.AccountId)
|
||||
{
|
||||
//Book is absent from the existing LibraryBook's account. Use the alternate account.
|
||||
existing.SetAccount(item.AccountId);
|
||||
}
|
||||
|
||||
existing.AbsentFromLastScan = isUnavailable(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing = new LibraryBook(
|
||||
bookImporter.Cache[item.DtoItem.ProductId],
|
||||
item.DtoItem.DateAdded,
|
||||
item.AccountId)
|
||||
{
|
||||
AbsentFromLastScan = isUnavailable(item)
|
||||
};
|
||||
existing.AbsentFromLastScan = isUnavailable(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing = new LibraryBook(
|
||||
bookImporter.Cache[item.DtoItem.ProductId],
|
||||
item.DtoItem.DateAdded,
|
||||
item.AccountId)
|
||||
{
|
||||
AbsentFromLastScan = isUnavailable(item)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(existing);
|
||||
qtyNew++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { existing.Book, existing.Account });
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(existing);
|
||||
qtyNew++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { existing.Book, existing.Account });
|
||||
}
|
||||
}
|
||||
|
||||
existing.SetIncludedUntil(item.DtoItem.GetExpirationDate());
|
||||
existing.SetIsAudiblePlus(item.DtoItem.IsAyce is true);
|
||||
}
|
||||
existing.SetIncludedUntil(item.DtoItem.GetExpirationDate());
|
||||
existing.SetIsAudiblePlus(item.DtoItem.IsAyce is true);
|
||||
}
|
||||
|
||||
var scannedAccounts = importItems.Select(i => i.AccountId).Distinct().ToHashSet();
|
||||
var allInScannedAccounts = DbContext.LibraryBooks.Where(lb => scannedAccounts.Contains(lb.Account)).ToArray();
|
||||
var scannedAccounts = importItems.Select(i => i.AccountId).Distinct().ToHashSet();
|
||||
var allInScannedAccounts = DbContext.LibraryBooks.Where(lb => scannedAccounts.Contains(lb.Account)).ToArray();
|
||||
|
||||
if (bookImporter.LoadedEntireLibrary)
|
||||
{
|
||||
if (bookImporter.LoadedEntireLibrary)
|
||||
{
|
||||
//If the entire library was loaded, all Books should be loaded onto their LibraryBook. HOWEVER,
|
||||
//a malformed Database may have a LibraryBook with a BookID that doesn't match any Book in the
|
||||
//Books table. In this case, the Books property will still be null we should also mark those
|
||||
//LibraryBooks as absent.
|
||||
//a malformed Database may have a LibraryBook with a BookID that doesn't match any Book in the
|
||||
//Books table. In this case, the Books property will still be null we should also mark those
|
||||
//LibraryBooks as absent.
|
||||
//Find LibraryBooks which have a Book but weren't found in the import, and mark them as absent.
|
||||
foreach (var absentBook in allInScannedAccounts.Where(lb => lb.Book?.AudibleProductId is not string asin || !uniqueImportItems.ContainsKey(asin)))
|
||||
absentBook.AbsentFromLastScan = true;
|
||||
absentBook.AbsentFromLastScan = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null.
|
||||
//Only change AbsentFromLastScan for LibraryBooks of accounts that were scanned.
|
||||
foreach (var nullBook in allInScannedAccounts.Where(lb => lb.Book is null && lb.Account.In(scannedAccounts)))
|
||||
nullBook.AbsentFromLastScan = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null.
|
||||
//Only change AbsentFromLastScan for LibraryBooks of accounts that were scanned.
|
||||
foreach (var nullBook in allInScannedAccounts.Where(lb => lb.Book is null && lb.Account.In(scannedAccounts)))
|
||||
nullBook.AbsentFromLastScan = true;
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private static Dictionary<TKey, TSource> ToDictionarySafe<TKey, TSource>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TSource, TSource> tieBreaker)
|
||||
where TKey : notnull
|
||||
private static Dictionary<TKey, TSource> ToDictionarySafe<TKey, TSource>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TSource, TSource> tieBreaker)
|
||||
where TKey : notnull
|
||||
{
|
||||
var dictionary = new Dictionary<TKey, TSource>();
|
||||
var dictionary = new Dictionary<TKey, TSource>();
|
||||
|
||||
foreach (TSource newItem in source)
|
||||
{
|
||||
TKey key = keySelector(newItem);
|
||||
foreach (TSource newItem in source)
|
||||
{
|
||||
TKey key = keySelector(newItem);
|
||||
|
||||
dictionary[key]
|
||||
= dictionary.TryGetValue(key, out TSource? existingItem)
|
||||
? tieBreaker(existingItem, newItem)
|
||||
: newItem;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
dictionary[key]
|
||||
= dictionary.TryGetValue(key, out TSource? existingItem)
|
||||
? tieBreaker(existingItem, newItem)
|
||||
: newItem;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static ImportItem tieBreak(ImportItem item1, ImportItem item2)
|
||||
=> isUnavailable(item1) && !isUnavailable(item2) ? item2 : item1;
|
||||
private static ImportItem tieBreak(ImportItem item1, ImportItem item2)
|
||||
=> isUnavailable(item1) && !isUnavailable(item2) ? item2 : item1;
|
||||
|
||||
private static bool isUnavailable(ImportItem item)
|
||||
=> isFutureRelease(item) || isPlusTitleUnavailable(item);
|
||||
private static bool isUnavailable(ImportItem item)
|
||||
=> isFutureRelease(item) || isPlusTitleUnavailable(item);
|
||||
|
||||
private static bool isFutureRelease(ImportItem item)
|
||||
=> item.DtoItem.IssueDate is DateTimeOffset dt && dt > DateTimeOffset.UtcNow;
|
||||
private static bool isFutureRelease(ImportItem item)
|
||||
=> item.DtoItem.IssueDate is DateTimeOffset dt && dt > DateTimeOffset.UtcNow;
|
||||
|
||||
private static bool isPlusTitleUnavailable(ImportItem item)
|
||||
=> item.DtoItem.ContentType is null
|
||||
|| (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true);
|
||||
private static bool isPlusTitleUnavailable(ImportItem item)
|
||||
=> item.DtoItem.ContentType is null
|
||||
|| (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true);
|
||||
}
|
||||
@@ -1,34 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace DtoImporterService
|
||||
namespace DtoImporterService;
|
||||
|
||||
public record timeLogEntry(string msg, long totalElapsed, long delta);
|
||||
public static class PerfLogger
|
||||
{
|
||||
public record timeLogEntry(string msg, long totalElapsed, long delta);
|
||||
public static class PerfLogger
|
||||
private static Stopwatch sw { get; } = new();
|
||||
private static List<timeLogEntry> __log { get; } = new() { new("begin", 0, 0) };
|
||||
|
||||
public static void logTime(string s)
|
||||
{
|
||||
private static Stopwatch sw { get; } = new();
|
||||
private static List<timeLogEntry> __log { get; } = new() { new("begin", 0, 0) };
|
||||
var totalElapsed = sw.ElapsedMilliseconds;
|
||||
|
||||
public static void logTime(string s)
|
||||
{
|
||||
var totalElapsed = sw.ElapsedMilliseconds;
|
||||
var prev = __log.Last().totalElapsed;
|
||||
var delta = totalElapsed - prev;
|
||||
|
||||
var prev = __log.Last().totalElapsed;
|
||||
var delta = totalElapsed - prev;
|
||||
|
||||
__log.Add(new(s, totalElapsed, delta));
|
||||
}
|
||||
public static void logRestart()
|
||||
{
|
||||
__log.Clear();
|
||||
__log.Add(new("begin", 0, 0));
|
||||
sw.Restart();
|
||||
}
|
||||
public static void stop() => sw.Stop();
|
||||
public static string logOutput =>
|
||||
$"{nameof(timeLogEntry.msg)}\t{nameof(timeLogEntry.totalElapsed)}\t{nameof(timeLogEntry.delta)}\r\n"
|
||||
+ __log.Select(t => $"{t.msg}\t{t.totalElapsed}\t{t.delta}").Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
__log.Add(new(s, totalElapsed, delta));
|
||||
}
|
||||
public static void logRestart()
|
||||
{
|
||||
__log.Clear();
|
||||
__log.Add(new("begin", 0, 0));
|
||||
sw.Restart();
|
||||
}
|
||||
public static void stop() => sw.Stop();
|
||||
public static string logOutput =>
|
||||
$"{nameof(timeLogEntry.msg)}\t{nameof(timeLogEntry.totalElapsed)}\t{nameof(timeLogEntry.delta)}\r\n"
|
||||
+ __log.Select(t => $"{t.msg}\t{t.totalElapsed}\t{t.delta}").Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
}
|
||||
|
||||
@@ -1,84 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DtoImporterService
|
||||
namespace DtoImporterService;
|
||||
|
||||
public class SeriesImporter : ItemsImporterBase
|
||||
{
|
||||
public class SeriesImporter : ItemsImporterBase
|
||||
protected override IValidator Validator => new SeriesValidator();
|
||||
|
||||
public Dictionary<string, DataLayer.Series> Cache { get; private set; } = new();
|
||||
|
||||
public SeriesImporter(LibationContext context) : base(context) { }
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
protected override IValidator Validator => new SeriesValidator();
|
||||
// get distinct
|
||||
var series = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetSeriesDistinct()
|
||||
.ToList();
|
||||
|
||||
public Dictionary<string, DataLayer.Series> Cache { get; private set; } = new();
|
||||
// load db existing => .Local
|
||||
loadLocal_series(series);
|
||||
|
||||
public SeriesImporter(LibationContext context) : base(context) { }
|
||||
// upsert
|
||||
var qtyNew = upsertSeries(series);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
private void loadLocal_series(List<AudibleApi.Common.Series> series)
|
||||
{
|
||||
var seriesIds = series.Select(s => s.SeriesId).Distinct().ToList();
|
||||
|
||||
if (seriesIds.Count != 0)
|
||||
Cache = DbContext.Series
|
||||
.Where(s => seriesIds.Contains(s.AudibleSeriesId))
|
||||
.ToDictionarySafe(s => s.AudibleSeriesId);
|
||||
}
|
||||
|
||||
private int upsertSeries(List<AudibleApi.Common.Series> requestedSeries)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var s in requestedSeries)
|
||||
{
|
||||
// get distinct
|
||||
var series = importItems
|
||||
.Select(i => i.DtoItem)
|
||||
.GetSeriesDistinct()
|
||||
.ToList();
|
||||
|
||||
// load db existing => .Local
|
||||
loadLocal_series(series);
|
||||
|
||||
// upsert
|
||||
var qtyNew = upsertSeries(series);
|
||||
return qtyNew;
|
||||
if (string.IsNullOrEmpty(s.SeriesId))
|
||||
continue;
|
||||
// AudibleApi.Common.Series.SeriesId == DataLayer.AudibleSeriesId
|
||||
if (!Cache.TryGetValue(s.SeriesId, out var series))
|
||||
{
|
||||
series = addSeries(s.SeriesId);
|
||||
qtyNew++;
|
||||
}
|
||||
series.UpdateName(s.SeriesName);
|
||||
}
|
||||
|
||||
private void loadLocal_series(List<AudibleApi.Common.Series> series)
|
||||
{
|
||||
var seriesIds = series.Select(s => s.SeriesId).Distinct().ToList();
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
if (seriesIds.Count != 0)
|
||||
Cache = DbContext.Series
|
||||
.Where(s => seriesIds.Contains(s.AudibleSeriesId))
|
||||
.ToDictionarySafe(s => s.AudibleSeriesId);
|
||||
private DataLayer.Series addSeries(string seriesId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var series = new DataLayer.Series(new AudibleSeriesId(seriesId));
|
||||
|
||||
var entityEntry = DbContext.Series.Add(series);
|
||||
var entity = entityEntry.Entity;
|
||||
|
||||
Cache.Add(entity.AudibleSeriesId, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private int upsertSeries(List<AudibleApi.Common.Series> requestedSeries)
|
||||
catch (Exception ex)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var s in requestedSeries)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s.SeriesId))
|
||||
continue;
|
||||
// AudibleApi.Common.Series.SeriesId == DataLayer.AudibleSeriesId
|
||||
if (!Cache.TryGetValue(s.SeriesId, out var series))
|
||||
{
|
||||
series = addSeries(s.SeriesId);
|
||||
qtyNew++;
|
||||
}
|
||||
series.UpdateName(s.SeriesName);
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private DataLayer.Series addSeries(string seriesId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var series = new DataLayer.Series(new AudibleSeriesId(seriesId));
|
||||
|
||||
var entityEntry = DbContext.Series.Add(series);
|
||||
var entity = entityEntry.Entity;
|
||||
|
||||
Cache.Add(entity.AudibleSeriesId, entity);
|
||||
return entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding series. {@DebugInfo}", new { seriesId });
|
||||
throw;
|
||||
}
|
||||
Serilog.Log.Logger.Error(ex, "Error adding series. {@DebugInfo}", new { seriesId });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,51 @@
|
||||
using LibationFileManager;
|
||||
using NAudio.Lame;
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FileLiberator
|
||||
namespace FileLiberator;
|
||||
|
||||
public abstract class AudioDecodable : Processable
|
||||
{
|
||||
public abstract class AudioDecodable : Processable
|
||||
{
|
||||
public delegate byte[]? RequestCoverArtHandler(object sender, EventArgs eventArgs);
|
||||
public event RequestCoverArtHandler? RequestCoverArt;
|
||||
public event EventHandler<string>? TitleDiscovered;
|
||||
public event EventHandler<string>? AuthorsDiscovered;
|
||||
public event EventHandler<string>? NarratorsDiscovered;
|
||||
public event EventHandler<byte[]>? CoverImageDiscovered;
|
||||
public abstract Task CancelAsync();
|
||||
public delegate byte[]? RequestCoverArtHandler(object sender, EventArgs eventArgs);
|
||||
public event RequestCoverArtHandler? RequestCoverArt;
|
||||
public event EventHandler<string>? TitleDiscovered;
|
||||
public event EventHandler<string>? AuthorsDiscovered;
|
||||
public event EventHandler<string>? NarratorsDiscovered;
|
||||
public event EventHandler<byte[]>? CoverImageDiscovered;
|
||||
public abstract Task CancelAsync();
|
||||
|
||||
protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title);
|
||||
protected void OnTitleDiscovered(object? _, string? title)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = title });
|
||||
if (title != null)
|
||||
TitleDiscovered?.Invoke(this, title);
|
||||
}
|
||||
protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title);
|
||||
protected void OnTitleDiscovered(object? _, string? title)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = title });
|
||||
if (title != null)
|
||||
TitleDiscovered?.Invoke(this, title);
|
||||
}
|
||||
|
||||
protected void OnAuthorsDiscovered(string authors) => OnAuthorsDiscovered(null, authors);
|
||||
protected void OnAuthorsDiscovered(object? _, string? authors)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = authors });
|
||||
if (authors != null)
|
||||
AuthorsDiscovered?.Invoke(this, authors);
|
||||
}
|
||||
protected void OnAuthorsDiscovered(string authors) => OnAuthorsDiscovered(null, authors);
|
||||
protected void OnAuthorsDiscovered(object? _, string? authors)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = authors });
|
||||
if (authors != null)
|
||||
AuthorsDiscovered?.Invoke(this, authors);
|
||||
}
|
||||
|
||||
protected void OnNarratorsDiscovered(string narrators) => OnNarratorsDiscovered(null, narrators);
|
||||
protected void OnNarratorsDiscovered(object? _, string? narrators)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = narrators });
|
||||
if (narrators != null)
|
||||
NarratorsDiscovered?.Invoke(this, narrators);
|
||||
}
|
||||
protected void OnNarratorsDiscovered(string narrators) => OnNarratorsDiscovered(null, narrators);
|
||||
protected void OnNarratorsDiscovered(object? _, string? narrators)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = narrators });
|
||||
if (narrators != null)
|
||||
NarratorsDiscovered?.Invoke(this, narrators);
|
||||
}
|
||||
|
||||
protected byte[]? OnRequestCoverArt()
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
|
||||
return RequestCoverArt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
protected byte[]? OnRequestCoverArt()
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
|
||||
return RequestCoverArt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected void OnCoverImageDiscovered(byte[] coverImage)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(CoverImageDiscovered), CoverImageBytes = coverImage.Length });
|
||||
CoverImageDiscovered?.Invoke(this, coverImage);
|
||||
}
|
||||
}
|
||||
protected void OnCoverImageDiscovered(byte[] coverImage)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(CoverImageDiscovered), CoverImageBytes = coverImage.Length });
|
||||
CoverImageDiscovered?.Invoke(this, coverImage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AaxDecrypter;
|
||||
using AaxDecrypter;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace FileLiberator
|
||||
namespace FileLiberator;
|
||||
|
||||
public static class AudioFileStorageExt
|
||||
{
|
||||
public static class AudioFileStorageExt
|
||||
/// <summary>
|
||||
/// DownloadDecryptBook:
|
||||
/// File path for where to move files into.
|
||||
/// Path: directory nested inside of Books directory
|
||||
/// File name: n/a
|
||||
/// </summary>
|
||||
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook, Configuration? config = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// DownloadDecryptBook:
|
||||
/// File path for where to move files into.
|
||||
/// Path: directory nested inside of Books directory
|
||||
/// File name: n/a
|
||||
/// </summary>
|
||||
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook, Configuration? config = null)
|
||||
{
|
||||
if (AudibleFileStorage.BooksDirectory is not { } books)
|
||||
throw new InvalidOperationException("Books directory is not set.");
|
||||
if (AudibleFileStorage.BooksDirectory is not { } books)
|
||||
throw new InvalidOperationException("Books directory is not set.");
|
||||
|
||||
config ??= Configuration.Instance;
|
||||
if (libraryBook.Book.IsEpisodeChild() && config.SavePodcastsToParentFolder)
|
||||
config ??= Configuration.Instance;
|
||||
if (libraryBook.Book.IsEpisodeChild() && config.SavePodcastsToParentFolder)
|
||||
{
|
||||
var series = libraryBook.Book.SeriesLink.SingleOrDefault();
|
||||
if (series is not null)
|
||||
{
|
||||
var series = libraryBook.Book.SeriesLink.SingleOrDefault();
|
||||
if (series is not null)
|
||||
LibraryBook? seriesParent = ApplicationServices.DbContexts.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId);
|
||||
if (seriesParent is not null)
|
||||
{
|
||||
LibraryBook? seriesParent = ApplicationServices.DbContexts.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId);
|
||||
if (seriesParent is not null)
|
||||
{
|
||||
return Templates.Folder.GetFilename(seriesParent.ToDto(), books, "");
|
||||
}
|
||||
return Templates.Folder.GetFilename(seriesParent.ToDto(), books, "");
|
||||
}
|
||||
}
|
||||
return Templates.Folder.GetFilename(libraryBook.ToDto(), books, "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file does not exist
|
||||
/// </summary>
|
||||
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension, bool returnFirstExisting = false)
|
||||
=> AudibleFileStorage.BooksDirectory is { } books ? Templates.File.GetFilename(libraryBook.ToDto(), books, extension, null, returnFirstExisting)
|
||||
: throw new InvalidOperationException("Books directory is not set.");
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file already exists
|
||||
/// </summary>
|
||||
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension, MultiConvertFileProperties? partProperties = null, bool returnFirstExisting = false)
|
||||
=> partProperties is null ? Templates.File.GetFilename(libraryBook.ToDto(), dirFullPath, extension, returnFirstExisting: returnFirstExisting)
|
||||
: Templates.ChapterFile.GetFilename(libraryBook.ToDto(), partProperties, dirFullPath, extension, returnFirstExisting: returnFirstExisting);
|
||||
return Templates.Folder.GetFilename(libraryBook.ToDto(), books, "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file does not exist
|
||||
/// </summary>
|
||||
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension, bool returnFirstExisting = false)
|
||||
=> AudibleFileStorage.BooksDirectory is { } books ? Templates.File.GetFilename(libraryBook.ToDto(), books, extension, null, returnFirstExisting)
|
||||
: throw new InvalidOperationException("Books directory is not set.");
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file already exists
|
||||
/// </summary>
|
||||
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension, MultiConvertFileProperties? partProperties = null, bool returnFirstExisting = false)
|
||||
=> partProperties is null ? Templates.File.GetFilename(libraryBook.ToDto(), dirFullPath, extension, returnFirstExisting: returnFirstExisting)
|
||||
: Templates.ChapterFile.GetFilename(libraryBook.ToDto(), partProperties, dirFullPath, extension, returnFirstExisting: returnFirstExisting);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
namespace AaxDecrypter;
|
||||
|
||||
/// <summary> Read audio codec, bitrate, sample rate, and channel count from MP4 and MP3 audio files. </summary>
|
||||
@@ -218,10 +217,10 @@ public static class AudioFormatDecoder
|
||||
|
||||
private static byte GetSideInfo(bool stereo, Version version) => (stereo, version) switch
|
||||
{
|
||||
(true, Version.Version_1) => 32,
|
||||
(true, Version.Version_2 or Version.Version_2_5) => 17,
|
||||
(false, Version.Version_1) => 17,
|
||||
(false, Version.Version_2 or Version.Version_2_5) => 9,
|
||||
(true, Version.Version_1) => 32,
|
||||
(true, Version.Version_2 or Version.Version_2_5) => 17,
|
||||
(false, Version.Version_1) => 17,
|
||||
(false, Version.Version_2 or Version.Version_2_5) => 9,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,176 +1,175 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AAXClean;
|
||||
using AAXClean;
|
||||
using AAXClean.Codecs;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FileLiberator
|
||||
namespace FileLiberator;
|
||||
|
||||
public class ConvertToMp3 : AudioDecodable, IProcessable<ConvertToMp3>
|
||||
{
|
||||
public class ConvertToMp3 : AudioDecodable, IProcessable<ConvertToMp3>
|
||||
public override string Name => "Convert to Mp3";
|
||||
private Mp4Operation? Mp4Operation;
|
||||
private readonly AaxDecrypter.AverageSpeed averageSpeed = new();
|
||||
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
||||
|
||||
private CancellationTokenSource? CancellationTokenSource { get; set; }
|
||||
public override async Task CancelAsync()
|
||||
{
|
||||
public override string Name => "Convert to Mp3";
|
||||
private Mp4Operation? Mp4Operation;
|
||||
private readonly AaxDecrypter.AverageSpeed averageSpeed = new();
|
||||
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
||||
|
||||
private CancellationTokenSource? CancellationTokenSource { get; set; }
|
||||
public override async Task CancelAsync()
|
||||
{
|
||||
if (CancellationTokenSource is not null)
|
||||
await CancellationTokenSource.CancelAsync();
|
||||
if (Mp4Operation is not null)
|
||||
await Mp4Operation.CancelAsync();
|
||||
}
|
||||
|
||||
public static bool ValidateMp3(LibraryBook libraryBook)
|
||||
{
|
||||
var paths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId);
|
||||
return paths.Any(path => path?.ToString()?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path)));
|
||||
}
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
OnBegin(libraryBook);
|
||||
var cancellationToken = (CancellationTokenSource = new()).Token;
|
||||
|
||||
try
|
||||
{
|
||||
var m4bPaths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId)
|
||||
.Where(m4bPath => File.Exists(m4bPath))
|
||||
.Select(m4bPath => new { m4bPath, proposedMp3Path = Mp3FileName(m4bPath), m4bSize = new FileInfo(m4bPath).Length })
|
||||
.Where(p => !File.Exists(p.proposedMp3Path))
|
||||
.ToArray();
|
||||
|
||||
long totalInputSize = m4bPaths.Sum(p => p.m4bSize);
|
||||
long sizeOfCompletedFiles = 0L;
|
||||
foreach (var entry in m4bPaths)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (File.Exists(entry.proposedMp3Path) || !File.Exists(entry.m4bPath))
|
||||
{
|
||||
sizeOfCompletedFiles += entry.m4bSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
using var m4bFileStream = File.Open(entry.m4bPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var m4bBook = new Mp4File(m4bFileStream);
|
||||
|
||||
if (m4bBook.MetadataItems.Title is string title)
|
||||
OnTitleDiscovered(title);
|
||||
if (m4bBook.MetadataItems.FirstAuthor is string firstAuthor)
|
||||
OnAuthorsDiscovered(firstAuthor);
|
||||
if (m4bBook.MetadataItems.Narrator is string narrator)
|
||||
OnNarratorsDiscovered(narrator);
|
||||
if (m4bBook.MetadataItems.Cover is byte[] cover)
|
||||
OnCoverImageDiscovered(cover);
|
||||
|
||||
var lameConfig = DownloadOptions.GetLameOptions(Configuration);
|
||||
var chapters = m4bBook.GetChaptersFromMetadata();
|
||||
//Finishing configuring lame encoder.
|
||||
AaxDecrypter.MpegUtil.ConfigureLameOptions(
|
||||
m4bBook,
|
||||
lameConfig,
|
||||
Configuration.LameDownsampleMono,
|
||||
Configuration.LameMatchSourceBR,
|
||||
chapters);
|
||||
|
||||
if (m4bBook.MetadataItems.TrackNumber is { } trackNum && lameConfig.ID3 is not null)
|
||||
{
|
||||
lameConfig.ID3.Track = trackNum.TotalTracks > 0 ? $"{trackNum.Track}/{trackNum.TotalTracks}" : trackNum.Track.ToString();
|
||||
}
|
||||
|
||||
long currentFileNumBytesProcessed = 0;
|
||||
try
|
||||
{
|
||||
var tempPath = Path.GetTempFileName();
|
||||
using (var mp3File = File.Open(tempPath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
|
||||
{
|
||||
Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters);
|
||||
Mp4Operation.ConversionProgressUpdate += m4bBook_ConversionProgressUpdate;
|
||||
await Mp4Operation;
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
FileUtility.SaferDelete(tempPath);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var realMp3Path
|
||||
= FileUtility.SaferMoveToValidPath(
|
||||
tempPath,
|
||||
entry.proposedMp3Path,
|
||||
Configuration.ReplacementCharacters,
|
||||
extension: "mp3",
|
||||
Configuration.OverwriteExisting);
|
||||
|
||||
SetFileTime(libraryBook, realMp3Path);
|
||||
if (Path.GetDirectoryName(realMp3Path) is string outputDir)
|
||||
SetDirectoryTime(libraryBook, outputDir);
|
||||
OnFileCreated(libraryBook, realMp3Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Mp4Operation is not null)
|
||||
Mp4Operation.ConversionProgressUpdate -= m4bBook_ConversionProgressUpdate;
|
||||
|
||||
sizeOfCompletedFiles += entry.m4bSize;
|
||||
}
|
||||
void m4bBook_ConversionProgressUpdate(object? sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
currentFileNumBytesProcessed = (long)(e.FractionCompleted * entry.m4bSize);
|
||||
var bytesCompleted = sizeOfCompletedFiles + currentFileNumBytesProcessed;
|
||||
ConversionProgressUpdate(totalInputSize, bytesCompleted);
|
||||
}
|
||||
}
|
||||
return new StatusHandler();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Serilog.Log.Error(ex, "AAXClean error");
|
||||
return new StatusHandler { "Conversion failed" };
|
||||
}
|
||||
return new StatusHandler { "Cancelled" };
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnCompleted(libraryBook);
|
||||
CancellationTokenSource.Dispose();
|
||||
CancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConversionProgressUpdate(long totalInputSize, long bytesCompleted)
|
||||
{
|
||||
averageSpeed.AddPosition(bytesCompleted);
|
||||
|
||||
var remainingBytes = (totalInputSize - bytesCompleted);
|
||||
var estTimeRemaining = remainingBytes / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * bytesCompleted / totalInputSize;
|
||||
|
||||
OnStreamingProgressChanged(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = bytesCompleted,
|
||||
TotalBytesToReceive = totalInputSize
|
||||
});
|
||||
}
|
||||
public static ConvertToMp3 Create(Configuration config) => new() { Configuration = config };
|
||||
private ConvertToMp3() { }
|
||||
if (CancellationTokenSource is not null)
|
||||
await CancellationTokenSource.CancelAsync();
|
||||
if (Mp4Operation is not null)
|
||||
await Mp4Operation.CancelAsync();
|
||||
}
|
||||
|
||||
public static bool ValidateMp3(LibraryBook libraryBook)
|
||||
{
|
||||
var paths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId);
|
||||
return paths.Any(path => path?.ToString()?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path)));
|
||||
}
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
OnBegin(libraryBook);
|
||||
var cancellationToken = (CancellationTokenSource = new()).Token;
|
||||
|
||||
try
|
||||
{
|
||||
var m4bPaths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId)
|
||||
.Where(m4bPath => File.Exists(m4bPath))
|
||||
.Select(m4bPath => new { m4bPath, proposedMp3Path = Mp3FileName(m4bPath), m4bSize = new FileInfo(m4bPath).Length })
|
||||
.Where(p => !File.Exists(p.proposedMp3Path))
|
||||
.ToArray();
|
||||
|
||||
long totalInputSize = m4bPaths.Sum(p => p.m4bSize);
|
||||
long sizeOfCompletedFiles = 0L;
|
||||
foreach (var entry in m4bPaths)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (File.Exists(entry.proposedMp3Path) || !File.Exists(entry.m4bPath))
|
||||
{
|
||||
sizeOfCompletedFiles += entry.m4bSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
using var m4bFileStream = File.Open(entry.m4bPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var m4bBook = new Mp4File(m4bFileStream);
|
||||
|
||||
if (m4bBook.MetadataItems.Title is string title)
|
||||
OnTitleDiscovered(title);
|
||||
if (m4bBook.MetadataItems.FirstAuthor is string firstAuthor)
|
||||
OnAuthorsDiscovered(firstAuthor);
|
||||
if (m4bBook.MetadataItems.Narrator is string narrator)
|
||||
OnNarratorsDiscovered(narrator);
|
||||
if (m4bBook.MetadataItems.Cover is byte[] cover)
|
||||
OnCoverImageDiscovered(cover);
|
||||
|
||||
var lameConfig = DownloadOptions.GetLameOptions(Configuration);
|
||||
var chapters = m4bBook.GetChaptersFromMetadata();
|
||||
//Finishing configuring lame encoder.
|
||||
AaxDecrypter.MpegUtil.ConfigureLameOptions(
|
||||
m4bBook,
|
||||
lameConfig,
|
||||
Configuration.LameDownsampleMono,
|
||||
Configuration.LameMatchSourceBR,
|
||||
chapters);
|
||||
|
||||
if (m4bBook.MetadataItems.TrackNumber is { } trackNum && lameConfig.ID3 is not null)
|
||||
{
|
||||
lameConfig.ID3.Track = trackNum.TotalTracks > 0 ? $"{trackNum.Track}/{trackNum.TotalTracks}" : trackNum.Track.ToString();
|
||||
}
|
||||
|
||||
long currentFileNumBytesProcessed = 0;
|
||||
try
|
||||
{
|
||||
var tempPath = Path.GetTempFileName();
|
||||
using (var mp3File = File.Open(tempPath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
|
||||
{
|
||||
Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters);
|
||||
Mp4Operation.ConversionProgressUpdate += m4bBook_ConversionProgressUpdate;
|
||||
await Mp4Operation;
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
FileUtility.SaferDelete(tempPath);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var realMp3Path
|
||||
= FileUtility.SaferMoveToValidPath(
|
||||
tempPath,
|
||||
entry.proposedMp3Path,
|
||||
Configuration.ReplacementCharacters,
|
||||
extension: "mp3",
|
||||
Configuration.OverwriteExisting);
|
||||
|
||||
SetFileTime(libraryBook, realMp3Path);
|
||||
if (Path.GetDirectoryName(realMp3Path) is string outputDir)
|
||||
SetDirectoryTime(libraryBook, outputDir);
|
||||
OnFileCreated(libraryBook, realMp3Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Mp4Operation is not null)
|
||||
Mp4Operation.ConversionProgressUpdate -= m4bBook_ConversionProgressUpdate;
|
||||
|
||||
sizeOfCompletedFiles += entry.m4bSize;
|
||||
}
|
||||
void m4bBook_ConversionProgressUpdate(object? sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
currentFileNumBytesProcessed = (long)(e.FractionCompleted * entry.m4bSize);
|
||||
var bytesCompleted = sizeOfCompletedFiles + currentFileNumBytesProcessed;
|
||||
ConversionProgressUpdate(totalInputSize, bytesCompleted);
|
||||
}
|
||||
}
|
||||
return new StatusHandler();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Serilog.Log.Error(ex, "AAXClean error");
|
||||
return new StatusHandler { "Conversion failed" };
|
||||
}
|
||||
return new StatusHandler { "Cancelled" };
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnCompleted(libraryBook);
|
||||
CancellationTokenSource.Dispose();
|
||||
CancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConversionProgressUpdate(long totalInputSize, long bytesCompleted)
|
||||
{
|
||||
averageSpeed.AddPosition(bytesCompleted);
|
||||
|
||||
var remainingBytes = (totalInputSize - bytesCompleted);
|
||||
var estTimeRemaining = remainingBytes / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * bytesCompleted / totalInputSize;
|
||||
|
||||
OnStreamingProgressChanged(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = bytesCompleted,
|
||||
TotalBytesToReceive = totalInputSize
|
||||
});
|
||||
}
|
||||
public static ConvertToMp3 Create(Configuration config) => new() { Configuration = config };
|
||||
private ConvertToMp3() { }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace FileLiberator;
|
||||
|
||||
public partial class DownloadOptions
|
||||
|
||||
@@ -1,92 +1,90 @@
|
||||
using AaxDecrypter;
|
||||
using Dinah.Core;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
using System;
|
||||
using System.IO;
|
||||
using LibationFileManager.Templates;
|
||||
|
||||
#nullable enable
|
||||
namespace FileLiberator
|
||||
namespace FileLiberator;
|
||||
|
||||
public partial class DownloadOptions : IDownloadOptions, IDisposable
|
||||
{
|
||||
public partial class DownloadOptions : IDownloadOptions, IDisposable
|
||||
public event EventHandler<long>? DownloadSpeedChanged;
|
||||
public LibraryBook LibraryBook { get; }
|
||||
public LibraryBookDto LibraryBookDto { get; }
|
||||
public string DownloadUrl { get; }
|
||||
public KeyData[]? DecryptionKeys { get; }
|
||||
public required TimeSpan RuntimeLength { get; init; }
|
||||
public OutputFormat OutputFormat { get; }
|
||||
public required Mpeg4Lib.ChapterInfo ChapterInfo { get; init; }
|
||||
public string Title => LibraryBook.Book.Title;
|
||||
public string Subtitle => LibraryBook.Book.Subtitle;
|
||||
public string? Publisher => LibraryBook.Book.Publisher;
|
||||
public string? Language => LibraryBook.Book.Language;
|
||||
public string AudibleProductId => LibraryBookDto.AudibleProductId;
|
||||
public string? SeriesName => LibraryBookDto.FirstSeries?.Name;
|
||||
public string? SeriesNumber => LibraryBookDto.FirstSeries?.Order?.ToString();
|
||||
public NAudio.Lame.LameConfig? LameConfig { get; }
|
||||
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
|
||||
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
|
||||
public bool CreateCueSheet => Config.CreateCueSheet;
|
||||
public long DownloadSpeedBps => Config.DownloadSpeedLimit;
|
||||
public bool FixupFile => Config.AllowLibationFixup;
|
||||
public bool Downsample => Config.AllowLibationFixup && Config.LameDownsampleMono;
|
||||
public bool MatchSourceBitrate => Config.AllowLibationFixup && Config.LameMatchSourceBR && Config.LameTargetBitrate;
|
||||
public bool MoveMoovToBeginning => Config.MoveMoovToBeginning;
|
||||
public AAXClean.FileType? InputType { get; }
|
||||
public AudibleApi.Common.DrmType DrmType { get; }
|
||||
public AudibleApi.Common.ContentMetadata ContentMetadata { get; }
|
||||
|
||||
public string GetMultipartTitle(MultiConvertFileProperties props)
|
||||
=> Templates.ChapterTitle.GetName(LibraryBookDto, props);
|
||||
|
||||
public Configuration Config { get; }
|
||||
private readonly IDisposable cancellation;
|
||||
public void Dispose()
|
||||
{
|
||||
public event EventHandler<long>? DownloadSpeedChanged;
|
||||
public LibraryBook LibraryBook { get; }
|
||||
public LibraryBookDto LibraryBookDto { get; }
|
||||
public string DownloadUrl { get; }
|
||||
public KeyData[]? DecryptionKeys { get; }
|
||||
public required TimeSpan RuntimeLength { get; init; }
|
||||
public OutputFormat OutputFormat { get; }
|
||||
public required Mpeg4Lib.ChapterInfo ChapterInfo { get; init; }
|
||||
public string Title => LibraryBook.Book.Title;
|
||||
public string Subtitle => LibraryBook.Book.Subtitle;
|
||||
public string? Publisher => LibraryBook.Book.Publisher;
|
||||
public string? Language => LibraryBook.Book.Language;
|
||||
public string AudibleProductId => LibraryBookDto.AudibleProductId;
|
||||
public string? SeriesName => LibraryBookDto.FirstSeries?.Name;
|
||||
public string? SeriesNumber => LibraryBookDto.FirstSeries?.Order?.ToString();
|
||||
public NAudio.Lame.LameConfig? LameConfig { get; }
|
||||
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
|
||||
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
|
||||
public bool CreateCueSheet => Config.CreateCueSheet;
|
||||
public long DownloadSpeedBps => Config.DownloadSpeedLimit;
|
||||
public bool FixupFile => Config.AllowLibationFixup;
|
||||
public bool Downsample => Config.AllowLibationFixup && Config.LameDownsampleMono;
|
||||
public bool MatchSourceBitrate => Config.AllowLibationFixup && Config.LameMatchSourceBR && Config.LameTargetBitrate;
|
||||
public bool MoveMoovToBeginning => Config.MoveMoovToBeginning;
|
||||
public AAXClean.FileType? InputType { get; }
|
||||
public AudibleApi.Common.DrmType DrmType { get; }
|
||||
public AudibleApi.Common.ContentMetadata ContentMetadata { get; }
|
||||
cancellation?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public string GetMultipartTitle(MultiConvertFileProperties props)
|
||||
=> Templates.ChapterTitle.GetName(LibraryBookDto, props);
|
||||
private DownloadOptions(Configuration config, LibraryBook libraryBook, LicenseInfo licInfo)
|
||||
{
|
||||
Config = ArgumentValidator.EnsureNotNull(config, nameof(config));
|
||||
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||
|
||||
public Configuration Config { get; }
|
||||
private readonly IDisposable cancellation;
|
||||
public void Dispose()
|
||||
{
|
||||
cancellation?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
ArgumentValidator.EnsureNotNull(licInfo, nameof(licInfo));
|
||||
|
||||
private DownloadOptions(Configuration config, LibraryBook libraryBook, LicenseInfo licInfo)
|
||||
{
|
||||
Config = ArgumentValidator.EnsureNotNull(config, nameof(config));
|
||||
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||
if (licInfo.ContentMetadata.ContentUrl?.OfflineUrl is not string licUrl)
|
||||
throw new InvalidDataException("Content license doesn't contain an offline Url");
|
||||
|
||||
ArgumentValidator.EnsureNotNull(licInfo, nameof(licInfo));
|
||||
DownloadUrl = licUrl;
|
||||
DecryptionKeys = licInfo.DecryptionKeys;
|
||||
DrmType = licInfo.DrmType;
|
||||
ContentMetadata = licInfo.ContentMetadata;
|
||||
InputType
|
||||
= licInfo.DrmType is AudibleApi.Common.DrmType.Widevine ? AAXClean.FileType.Dash
|
||||
: licInfo.DrmType is AudibleApi.Common.DrmType.Adrm && licInfo.DecryptionKeys?.Length == 1 && licInfo.DecryptionKeys[0].KeyPart1.Length == 4 && licInfo.DecryptionKeys[0].KeyPart2 is null ? AAXClean.FileType.Aax
|
||||
: licInfo.DrmType is AudibleApi.Common.DrmType.Adrm && licInfo.DecryptionKeys?.Length == 1 && licInfo.DecryptionKeys[0].KeyPart1.Length == 16 && licInfo.DecryptionKeys[0].KeyPart2?.Length == 16 ? AAXClean.FileType.Aaxc
|
||||
: null;
|
||||
|
||||
if (licInfo.ContentMetadata.ContentUrl?.OfflineUrl is not string licUrl)
|
||||
throw new InvalidDataException("Content license doesn't contain an offline Url");
|
||||
//If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
|
||||
OutputFormat
|
||||
= licInfo.DrmType is not AudibleApi.Common.DrmType.Adrm and not AudibleApi.Common.DrmType.Widevine ||
|
||||
(config.AllowLibationFixup && config.DecryptToLossy)
|
||||
? OutputFormat.Mp3
|
||||
: OutputFormat.M4b;
|
||||
|
||||
DownloadUrl = licUrl;
|
||||
DecryptionKeys = licInfo.DecryptionKeys;
|
||||
DrmType = licInfo.DrmType;
|
||||
ContentMetadata = licInfo.ContentMetadata;
|
||||
InputType
|
||||
= licInfo.DrmType is AudibleApi.Common.DrmType.Widevine ? AAXClean.FileType.Dash
|
||||
: licInfo.DrmType is AudibleApi.Common.DrmType.Adrm && licInfo.DecryptionKeys?.Length == 1 && licInfo.DecryptionKeys[0].KeyPart1.Length == 4 && licInfo.DecryptionKeys[0].KeyPart2 is null ? AAXClean.FileType.Aax
|
||||
: licInfo.DrmType is AudibleApi.Common.DrmType.Adrm && licInfo.DecryptionKeys?.Length == 1 && licInfo.DecryptionKeys[0].KeyPart1.Length == 16 && licInfo.DecryptionKeys[0].KeyPart2?.Length == 16 ? AAXClean.FileType.Aaxc
|
||||
: null;
|
||||
LameConfig = OutputFormat == OutputFormat.Mp3 ? GetLameOptions(config) : null;
|
||||
|
||||
//If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
|
||||
OutputFormat
|
||||
= licInfo.DrmType is not AudibleApi.Common.DrmType.Adrm and not AudibleApi.Common.DrmType.Widevine ||
|
||||
(config.AllowLibationFixup && config.DecryptToLossy)
|
||||
? OutputFormat.Mp3
|
||||
: OutputFormat.M4b;
|
||||
// no null/empty check for key/iv. unencrypted files do not have them
|
||||
LibraryBookDto = LibraryBook.ToDto();
|
||||
|
||||
LameConfig = OutputFormat == OutputFormat.Mp3 ? GetLameOptions(config) : null;
|
||||
|
||||
// no null/empty check for key/iv. unencrypted files do not have them
|
||||
LibraryBookDto = LibraryBook.ToDto();
|
||||
|
||||
cancellation =
|
||||
config
|
||||
.ObservePropertyChanged<long>(
|
||||
nameof(Configuration.DownloadSpeedLimit),
|
||||
newVal => DownloadSpeedChanged?.Invoke(this, newVal));
|
||||
}
|
||||
cancellation =
|
||||
config
|
||||
.ObservePropertyChanged<long>(
|
||||
nameof(Configuration.DownloadSpeedLimit),
|
||||
newVal => DownloadSpeedChanged?.Invoke(this, newVal));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
namespace FileLiberator;
|
||||
|
||||
public class DownloadPdf : Processable, IProcessable<DownloadPdf>
|
||||
{
|
||||
public class DownloadPdf : Processable, IProcessable<DownloadPdf>
|
||||
public override string Name => "Download Pdf";
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !libraryBook.Book.PdfExists;
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
public override string Name => "Download Pdf";
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !libraryBook.Book.PdfExists;
|
||||
OnBegin(libraryBook);
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
try
|
||||
{
|
||||
OnBegin(libraryBook);
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
var result = verifyDownload(actualDownloadedFilePath);
|
||||
|
||||
try
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
var result = verifyDownload(actualDownloadedFilePath);
|
||||
SetFileTime(libraryBook, actualDownloadedFilePath);
|
||||
if (Path.GetDirectoryName(actualDownloadedFilePath) is string outputDir)
|
||||
SetDirectoryTime(libraryBook, outputDir);
|
||||
}
|
||||
await libraryBook.UpdatePdfStatusAsync(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
SetFileTime(libraryBook, actualDownloadedFilePath);
|
||||
if (Path.GetDirectoryName(actualDownloadedFilePath) is string outputDir)
|
||||
SetDirectoryTime(libraryBook, outputDir);
|
||||
}
|
||||
await libraryBook.UpdatePdfStatusAsync(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading PDF");
|
||||
|
||||
var result = new StatusHandler();
|
||||
result.AddError($"Error downloading PDF. See log for details. Error summary: {ex.Message}");
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnCompleted(libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
var extension = Path.GetExtension(getdownloadUrl(libraryBook)) ?? ".pdf";
|
||||
|
||||
// if audio file exists, get it's dir. else return base Book dir
|
||||
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
|
||||
if (existingPath is not null)
|
||||
return AudibleFileStorage.Audio.GetCustomDirFilename(libraryBook, existingPath, extension);
|
||||
|
||||
return AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, extension);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? getdownloadUrl(LibraryBook libraryBook)
|
||||
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
||||
|
||||
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
catch (Exception ex)
|
||||
{
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading PDF");
|
||||
|
||||
var progress = new Progress<DownloadProgress>(OnStreamingProgressChanged);
|
||||
var result = new StatusHandler();
|
||||
result.AddError($"Error downloading PDF. See log for details. Error summary: {ex.Message}");
|
||||
|
||||
var client = new HttpClient();
|
||||
|
||||
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||
OnFileCreated(libraryBook, actualDownloadedFilePath);
|
||||
|
||||
OnStatusUpdate(actualDownloadedFilePath);
|
||||
return actualDownloadedFilePath;
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnCompleted(libraryBook);
|
||||
}
|
||||
|
||||
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
|
||||
=> !File.Exists(actualDownloadedFilePath)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
|
||||
public static DownloadPdf Create(Configuration config) => new() { Configuration = config };
|
||||
private DownloadPdf() { }
|
||||
}
|
||||
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
var extension = Path.GetExtension(getdownloadUrl(libraryBook)) ?? ".pdf";
|
||||
|
||||
// if audio file exists, get it's dir. else return base Book dir
|
||||
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
|
||||
if (existingPath is not null)
|
||||
return AudibleFileStorage.Audio.GetCustomDirFilename(libraryBook, existingPath, extension);
|
||||
|
||||
return AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, extension);
|
||||
}
|
||||
|
||||
private static string? getdownloadUrl(LibraryBook libraryBook)
|
||||
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
||||
|
||||
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
{
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
var progress = new Progress<DownloadProgress>(OnStreamingProgressChanged);
|
||||
|
||||
var client = new HttpClient();
|
||||
|
||||
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||
OnFileCreated(libraryBook, actualDownloadedFilePath);
|
||||
|
||||
OnStatusUpdate(actualDownloadedFilePath);
|
||||
return actualDownloadedFilePath;
|
||||
}
|
||||
|
||||
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
|
||||
=> !File.Exists(actualDownloadedFilePath)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
|
||||
public static DownloadPdf Create(Configuration config) => new() { Configuration = config };
|
||||
private DownloadPdf() { }
|
||||
}
|
||||
|
||||
@@ -1,136 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace FileLiberator
|
||||
namespace FileLiberator;
|
||||
|
||||
public interface IProcessable<T> where T : IProcessable<T>
|
||||
{
|
||||
public interface IProcessable<T> where T : IProcessable<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new instance of the Processable which uses a specific Configuration
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="Configuration"/> this <typeparamref name="T"/> will use</param>
|
||||
static abstract T Create(Configuration config);
|
||||
}
|
||||
public abstract class Processable
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public event EventHandler<LibraryBook>? Begin;
|
||||
|
||||
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
|
||||
public event EventHandler<string>? StatusUpdate;
|
||||
/// <summary>Fired when a file is successfully saved to disk</summary>
|
||||
public event EventHandler<(string id, string path)>? FileCreated;
|
||||
public event EventHandler<DownloadProgress>? StreamingProgressChanged;
|
||||
public event EventHandler<TimeSpan>? StreamingTimeRemaining;
|
||||
|
||||
public event EventHandler<LibraryBook>? Completed;
|
||||
|
||||
public required Configuration Configuration{ get; init; }
|
||||
protected Processable() { }
|
||||
|
||||
/// <returns>True == Valid</returns>
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
|
||||
/// <returns>True == success</returns>
|
||||
public abstract Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);
|
||||
|
||||
// when used in foreach: stateful. deferred execution
|
||||
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
|
||||
=> library.Where(libraryBook =>
|
||||
Validate(libraryBook)
|
||||
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.DownloadEpisodes)
|
||||
);
|
||||
|
||||
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
|
||||
{
|
||||
if (validate && !Validate(libraryBook))
|
||||
return new StatusHandler { "Validation failed" };
|
||||
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(ProcessSingleAsync) + " {@DebugInfo}", new
|
||||
{
|
||||
libraryBook.Book.TitleWithSubtitle,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
libraryBook.Book.Locale,
|
||||
Account = libraryBook.Account?.ToMask() ?? "[empty]"
|
||||
});
|
||||
|
||||
var status
|
||||
= (await ProcessAsync(libraryBook))
|
||||
?? new StatusHandler { "Processable should never return a null status" };
|
||||
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task<StatusHandler> TryProcessAsync(LibraryBook libraryBook)
|
||||
=> Validate(libraryBook)
|
||||
? await ProcessAsync(libraryBook)
|
||||
: new StatusHandler();
|
||||
|
||||
protected void OnBegin(LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = libraryBook.LogFriendly() });
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
}
|
||||
|
||||
protected void OnStatusUpdate(string statusUpdate)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(StatusUpdate), Status = statusUpdate });
|
||||
StatusUpdate?.Invoke(this, statusUpdate);
|
||||
}
|
||||
|
||||
protected void OnFileCreated(LibraryBook libraryBook, string path)
|
||||
{
|
||||
Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), libraryBook.Book.AudibleProductId, path });
|
||||
FilePathCache.Insert(libraryBook.Book.AudibleProductId, path);
|
||||
FileCreated?.Invoke(this, (libraryBook.Book.AudibleProductId, path));
|
||||
}
|
||||
|
||||
protected void OnStreamingProgressChanged(DownloadProgress progress)
|
||||
=> OnStreamingProgressChanged(null, progress);
|
||||
protected void OnStreamingProgressChanged(object? _, DownloadProgress progress)
|
||||
=> StreamingProgressChanged?.Invoke(this, progress);
|
||||
|
||||
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining)
|
||||
=> OnStreamingTimeRemaining(null, timeRemaining);
|
||||
protected void OnStreamingTimeRemaining(object? _, TimeSpan timeRemaining)
|
||||
=> StreamingTimeRemaining?.Invoke(this, timeRemaining);
|
||||
|
||||
protected void OnCompleted(LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = libraryBook.LogFriendly() });
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
|
||||
protected void SetFileTime(LibraryBook libraryBook, string file)
|
||||
=> setFileSystemTime(libraryBook, new FileInfo(file));
|
||||
protected void SetDirectoryTime(LibraryBook libraryBook, string file)
|
||||
=> setFileSystemTime(libraryBook, new DirectoryInfo(file));
|
||||
|
||||
private void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists) return;
|
||||
|
||||
fileInfo.CreationTimeUtc = getTimeValue(Configuration.CreationTime) ?? fileInfo.CreationTimeUtc;
|
||||
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
|
||||
|
||||
DateTime? getTimeValue(Configuration.DateTimeSource source) => source switch
|
||||
{
|
||||
Configuration.DateTimeSource.Added => libraryBook.DateAdded,
|
||||
Configuration.DateTimeSource.Published => libraryBook.Book.DatePublished,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Create a new instance of the Processable which uses a specific Configuration
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="Configuration"/> this <typeparamref name="T"/> will use</param>
|
||||
static abstract T Create(Configuration config);
|
||||
}
|
||||
public abstract class Processable
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public event EventHandler<LibraryBook>? Begin;
|
||||
|
||||
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
|
||||
public event EventHandler<string>? StatusUpdate;
|
||||
/// <summary>Fired when a file is successfully saved to disk</summary>
|
||||
public event EventHandler<(string id, string path)>? FileCreated;
|
||||
public event EventHandler<DownloadProgress>? StreamingProgressChanged;
|
||||
public event EventHandler<TimeSpan>? StreamingTimeRemaining;
|
||||
|
||||
public event EventHandler<LibraryBook>? Completed;
|
||||
|
||||
public required Configuration Configuration { get; init; }
|
||||
protected Processable() { }
|
||||
|
||||
/// <returns>True == Valid</returns>
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
|
||||
/// <returns>True == success</returns>
|
||||
public abstract Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);
|
||||
|
||||
// when used in foreach: stateful. deferred execution
|
||||
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
|
||||
=> library.Where(libraryBook =>
|
||||
Validate(libraryBook)
|
||||
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.DownloadEpisodes)
|
||||
);
|
||||
|
||||
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
|
||||
{
|
||||
if (validate && !Validate(libraryBook))
|
||||
return new StatusHandler { "Validation failed" };
|
||||
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(ProcessSingleAsync) + " {@DebugInfo}", new
|
||||
{
|
||||
libraryBook.Book.TitleWithSubtitle,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
libraryBook.Book.Locale,
|
||||
Account = libraryBook.Account?.ToMask() ?? "[empty]"
|
||||
});
|
||||
|
||||
var status
|
||||
= (await ProcessAsync(libraryBook))
|
||||
?? new StatusHandler { "Processable should never return a null status" };
|
||||
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task<StatusHandler> TryProcessAsync(LibraryBook libraryBook)
|
||||
=> Validate(libraryBook)
|
||||
? await ProcessAsync(libraryBook)
|
||||
: new StatusHandler();
|
||||
|
||||
protected void OnBegin(LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = libraryBook.LogFriendly() });
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
}
|
||||
|
||||
protected void OnStatusUpdate(string statusUpdate)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(StatusUpdate), Status = statusUpdate });
|
||||
StatusUpdate?.Invoke(this, statusUpdate);
|
||||
}
|
||||
|
||||
protected void OnFileCreated(LibraryBook libraryBook, string path)
|
||||
{
|
||||
Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), libraryBook.Book.AudibleProductId, path });
|
||||
FilePathCache.Insert(libraryBook.Book.AudibleProductId, path);
|
||||
FileCreated?.Invoke(this, (libraryBook.Book.AudibleProductId, path));
|
||||
}
|
||||
|
||||
protected void OnStreamingProgressChanged(DownloadProgress progress)
|
||||
=> OnStreamingProgressChanged(null, progress);
|
||||
protected void OnStreamingProgressChanged(object? _, DownloadProgress progress)
|
||||
=> StreamingProgressChanged?.Invoke(this, progress);
|
||||
|
||||
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining)
|
||||
=> OnStreamingTimeRemaining(null, timeRemaining);
|
||||
protected void OnStreamingTimeRemaining(object? _, TimeSpan timeRemaining)
|
||||
=> StreamingTimeRemaining?.Invoke(this, timeRemaining);
|
||||
|
||||
protected void OnCompleted(LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = libraryBook.LogFriendly() });
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
|
||||
protected void SetFileTime(LibraryBook libraryBook, string file)
|
||||
=> setFileSystemTime(libraryBook, new FileInfo(file));
|
||||
protected void SetDirectoryTime(LibraryBook libraryBook, string file)
|
||||
=> setFileSystemTime(libraryBook, new DirectoryInfo(file));
|
||||
|
||||
private void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists) return;
|
||||
|
||||
fileInfo.CreationTimeUtc = getTimeValue(Configuration.CreationTime) ?? fileInfo.CreationTimeUtc;
|
||||
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
|
||||
|
||||
DateTime? getTimeValue(Configuration.DateTimeSource source) => source switch
|
||||
{
|
||||
Configuration.DateTimeSource.Added => libraryBook.DateAdded,
|
||||
Configuration.DateTimeSource.Published => libraryBook.Book.DatePublished,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,100 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleUtilities;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Authentication;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace FileLiberator
|
||||
namespace FileLiberator;
|
||||
|
||||
public static class UtilityExtensions
|
||||
{
|
||||
public static class UtilityExtensions
|
||||
public static (string id, string title, string locale, string account) LogFriendly(this LibraryBook libraryBook)
|
||||
=> (
|
||||
id: libraryBook.Book.AudibleProductId,
|
||||
title: libraryBook.Book.TitleWithSubtitle,
|
||||
locale: libraryBook.Book.Locale,
|
||||
account: libraryBook.Account.ToMask()
|
||||
);
|
||||
|
||||
public static Func<Account, Task<ApiExtended>>? ApiExtendedFunc { get; set; }
|
||||
|
||||
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
|
||||
{
|
||||
public static (string id, string title, string locale, string account) LogFriendly(this LibraryBook libraryBook)
|
||||
=> (
|
||||
id: libraryBook.Book.AudibleProductId,
|
||||
title: libraryBook.Book.TitleWithSubtitle,
|
||||
locale: libraryBook.Book.Locale,
|
||||
account: libraryBook.Account.ToMask()
|
||||
);
|
||||
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var account = accounts.AccountsSettings.GetAccount(libraryBook.Account, libraryBook.Book.Locale)
|
||||
?? throw new InvalidCredentialException($"No account found for '{libraryBook.Account}' and locale '{libraryBook.Book.Locale}'");
|
||||
|
||||
public static Func<Account, Task<ApiExtended>>? ApiExtendedFunc { get; set; }
|
||||
var apiExtended = await ApiExtended.CreateAsync(account);
|
||||
return apiExtended.Api;
|
||||
}
|
||||
|
||||
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
|
||||
public static bool SupportsWidevine(this AudibleApi.Api api)
|
||||
{
|
||||
//TODO: Expose Api's identity maintainer directly instead of using reflection.
|
||||
var identityProperty = api.GetType().GetProperty("_identityMaintainer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
return identityProperty?.GetValue(api) is AudibleApi.Authorization.IIdentityMaintainer identityMaintainer
|
||||
&& identityMaintainer.DeviceType == AudibleApi.Resources.DeviceType;
|
||||
}
|
||||
|
||||
public static LibraryBookDto ToDto(this LibraryBook libraryBook)
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var nickname
|
||||
= persister.AccountsSettings.Accounts
|
||||
.FirstOrDefault(a => a.AccountId == libraryBook.Account && a.Locale?.Name == libraryBook.Book.Locale)
|
||||
?.AccountName;
|
||||
|
||||
return new()
|
||||
{
|
||||
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var account = accounts.AccountsSettings.GetAccount(libraryBook.Account, libraryBook.Book.Locale)
|
||||
?? throw new InvalidCredentialException($"No account found for '{libraryBook.Account}' and locale '{libraryBook.Book.Locale}'");
|
||||
Account = libraryBook.Account,
|
||||
AccountNickname = nickname,
|
||||
DateAdded = libraryBook.DateAdded,
|
||||
|
||||
var apiExtended = await ApiExtended.CreateAsync(account);
|
||||
return apiExtended.Api;
|
||||
}
|
||||
AudibleProductId = libraryBook.Book.AudibleProductId,
|
||||
Title = libraryBook.Book.Title,
|
||||
Subtitle = libraryBook.Book.Subtitle,
|
||||
TitleWithSubtitle = libraryBook.Book.TitleWithSubtitle,
|
||||
Locale = libraryBook.Book.Locale,
|
||||
YearPublished = libraryBook.Book.DatePublished?.Year,
|
||||
DatePublished = libraryBook.Book.DatePublished,
|
||||
|
||||
public static bool SupportsWidevine(this AudibleApi.Api api)
|
||||
{
|
||||
//TODO: Expose Api's identity maintainer directly instead of using reflection.
|
||||
var identityProperty = api.GetType().GetProperty("_identityMaintainer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
return identityProperty?.GetValue(api) is AudibleApi.Authorization.IIdentityMaintainer identityMaintainer
|
||||
&& identityMaintainer.DeviceType == AudibleApi.Resources.DeviceType;
|
||||
}
|
||||
Authors = libraryBook.Book.Authors.Select(c => new ContributorDto(c.Name, c.AudibleContributorId)).ToList(),
|
||||
Narrators = libraryBook.Book.Narrators.Select(c => new ContributorDto(c.Name, c.AudibleContributorId)).ToList(),
|
||||
|
||||
public static LibraryBookDto ToDto(this LibraryBook libraryBook)
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var nickname
|
||||
= persister.AccountsSettings.Accounts
|
||||
.FirstOrDefault(a => a.AccountId == libraryBook.Account && a.Locale?.Name == libraryBook.Book.Locale)
|
||||
?.AccountName;
|
||||
Series = getSeries(libraryBook.Book.SeriesLink),
|
||||
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
|
||||
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
|
||||
|
||||
return new()
|
||||
{
|
||||
Account = libraryBook.Account,
|
||||
AccountNickname = nickname,
|
||||
DateAdded = libraryBook.DateAdded,
|
||||
Language = libraryBook.Book.Language,
|
||||
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.CodecString,
|
||||
BitRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.BitRate,
|
||||
SampleRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.SampleRate,
|
||||
Channels = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.ChannelCount,
|
||||
LibationVersion = libraryBook.Book.UserDefinedItem.LastDownloadedVersion.ToVersionString(),
|
||||
FileVersion = libraryBook.Book.UserDefinedItem.LastDownloadedFileVersion
|
||||
};
|
||||
}
|
||||
|
||||
AudibleProductId = libraryBook.Book.AudibleProductId,
|
||||
Title = libraryBook.Book.Title,
|
||||
Subtitle = libraryBook.Book.Subtitle,
|
||||
TitleWithSubtitle = libraryBook.Book.TitleWithSubtitle,
|
||||
Locale = libraryBook.Book.Locale,
|
||||
YearPublished = libraryBook.Book.DatePublished?.Year,
|
||||
DatePublished = libraryBook.Book.DatePublished,
|
||||
private static List<SeriesDto>? getSeries(IEnumerable<SeriesBook> seriesBooks)
|
||||
{
|
||||
if (!seriesBooks.Any())
|
||||
return null;
|
||||
|
||||
Authors = libraryBook.Book.Authors.Select(c => new ContributorDto(c.Name, c.AudibleContributorId)).ToList(),
|
||||
Narrators = libraryBook.Book.Narrators.Select(c => new ContributorDto(c.Name, c.AudibleContributorId)).ToList(),
|
||||
|
||||
Series = getSeries(libraryBook.Book.SeriesLink),
|
||||
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
|
||||
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
|
||||
|
||||
Language = libraryBook.Book.Language,
|
||||
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.CodecString,
|
||||
BitRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.BitRate,
|
||||
SampleRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.SampleRate,
|
||||
Channels = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.ChannelCount,
|
||||
LibationVersion = libraryBook.Book.UserDefinedItem.LastDownloadedVersion.ToVersionString(),
|
||||
FileVersion = libraryBook.Book.UserDefinedItem.LastDownloadedFileVersion
|
||||
};
|
||||
}
|
||||
|
||||
private static List<SeriesDto>? getSeries(IEnumerable<SeriesBook> seriesBooks)
|
||||
{
|
||||
if (!seriesBooks.Any())
|
||||
return null;
|
||||
|
||||
//I don't remember why or if there was a good reason not to have series numbers for
|
||||
//podcast parents, but preserving the behavior for backwards compatibility.
|
||||
return seriesBooks
|
||||
.Select(sb
|
||||
=> new SeriesDto(
|
||||
sb.Series.Name,
|
||||
sb.Book.IsEpisodeParent() ? null : sb.Order,
|
||||
sb.Series.AudibleSeriesId)
|
||||
).ToList();
|
||||
}
|
||||
//I don't remember why or if there was a good reason not to have series numbers for
|
||||
//podcast parents, but preserving the behavior for backwards compatibility.
|
||||
return seriesBooks
|
||||
.Select(sb
|
||||
=> new SeriesDto(
|
||||
sb.Series.Name,
|
||||
sb.Book.IsEpisodeParent() ? null : sb.Order,
|
||||
sb.Series.AudibleSeriesId)
|
||||
).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user