Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
25 changed files with 341 additions and 342 deletions
Showing only changes of commit 9214b0c87b - Show all commits

View File

@ -110,6 +110,7 @@ $routes = [
], ],
'anime.collection.view' => [ 'anime.collection.view' => [
'path' => '/anime-collection/view{/view}', 'path' => '/anime-collection/view{/view}',
'action' => 'view',
'tokens' => [ 'tokens' => [
'view' => ALPHA_SLUG_PATTERN, 'view' => ALPHA_SLUG_PATTERN,
], ],
@ -119,6 +120,12 @@ $routes = [
'action' => 'delete', 'action' => 'delete',
'verb' => 'post', 'verb' => 'post',
], ],
'anime.collection.redirect' => [
'path' => '/anime-collection',
],
'anime.collection.redirect2' => [
'path' => '/anime-collection/',
],
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Manga Collection Routes // Manga Collection Routes
// --------------------------------------------------------------------- // ---------------------------------------------------------------------

View File

@ -5,6 +5,9 @@
<?php if (empty($sections)): ?> <?php if (empty($sections)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<br />
<label>Filter: <input type='text' class='media-filter' /></label>
<br />
<?php foreach ($sections as $name => $items): ?> <?php foreach ($sections as $name => $items): ?>
<?php if (empty($items)): ?> <?php if (empty($items)): ?>
<section class="status"> <section class="status">

View File

@ -5,12 +5,15 @@
<?php if (empty($sections)): ?> <?php if (empty($sections)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<br />
<label>Filter: <input type='text' class='media-filter' /></label>
<br />
<?php foreach ($sections as $name => $items): ?> <?php foreach ($sections as $name => $items): ?>
<h2><?= $name ?></h2> <h2><?= $name ?></h2>
<?php if (empty($items)): ?> <?php if (empty($items)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<table> <table class='media-wrap'>
<thead> <thead>
<tr> <tr>
<?php if($auth->isAuthenticated()): ?> <?php if($auth->isAuthenticated()): ?>
@ -40,7 +43,7 @@
]) ?>">Edit</a> ]) ?>">Edit</a>
</td> </td>
<?php endif ?> <?php endif ?>
<td class="justify"> <td class="align-left justify">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]) ?>"> <a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]) ?>">
<?= $item['anime']['title'] ?> <?= $item['anime']['title'] ?>
</a> </a>

View File

@ -5,6 +5,9 @@
<?php if (empty($sections)): ?> <?php if (empty($sections)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<br />
<label>Filter: <input type='text' class='media-filter' /></label>
<br />
<div class="tabs"> <div class="tabs">
<?php $i = 0; ?> <?php $i = 0; ?>
<?php foreach ($sections as $name => $items): ?> <?php foreach ($sections as $name => $items): ?>
@ -19,6 +22,19 @@
</div> </div>
<?php $i++; ?> <?php $i++; ?>
<?php endforeach ?> <?php endforeach ?>
<!-- All Tab -->
<input type='radio' id='collection-tab-<?= $i ?>' name='collection-tabs' />
<label for='collection-tab-<?= $i ?>'><h2>All</h2></label>
<div class='content full-height'>
<?php foreach ($sections as $name => $items): ?>
<h3><?= $name ?></h3>
<section class="media-wrap">
<?php foreach ($items as $item): ?>
<?php include __DIR__ . '/cover-item.php'; ?>
<?php endforeach ?>
</section>
<?php endforeach ?>
</div>
</div> </div>
<?php endif ?> <?php endif ?>
</main> </main>

View File

@ -26,7 +26,7 @@
<td class="align-left"> <td class="align-left">
<select name="media_id" id="media_id"> <select name="media_id" id="media_id">
<?php foreach($media_items as $id => $name): ?> <?php foreach($media_items as $id => $name): ?>
<option <?= $item['media_id'] === (string)$id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option> <option <?= $item['media_id'] === $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option>
<?php endforeach ?> <?php endforeach ?>
</select> </select>
</td> </td>

View File

@ -5,6 +5,9 @@
<?php if (empty($sections)): ?> <?php if (empty($sections)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<br />
<label>Filter: <input type='text' class='media-filter' /></label>
<br />
<?php $i = 0; ?> <?php $i = 0; ?>
<div class="tabs"> <div class="tabs">
<?php foreach ($sections as $name => $items): ?> <?php foreach ($sections as $name => $items): ?>
@ -12,7 +15,7 @@
name="collection-tabs"/> name="collection-tabs"/>
<label for="collection-tab-<?= $i ?>"><h2><?= $name ?></h2></label> <label for="collection-tab-<?= $i ?>"><h2><?= $name ?></h2></label>
<div class="content full-height"> <div class="content full-height">
<table class="full-width"> <table class="full-width media-wrap">
<thead> <thead>
<tr> <tr>
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
@ -36,6 +39,35 @@
</div> </div>
<?php $i++ ?> <?php $i++ ?>
<?php endforeach ?> <?php endforeach ?>
<!-- All -->
<input type='radio' id='collection-tab-<?= $i ?>' name='collection-tabs' />
<label for='collection-tab-<?= $i ?>'><h2>All</h2></label>
<div class="content full-height">
<?php foreach ($sections as $name => $items): ?>
<h3><?= $name ?></h3>
<table class="full-width media-wrap">
<thead>
<tr>
<?php if ($auth->isAuthenticated()): ?>
<td>Actions</td>
<?php endif ?>
<th>Title</th>
<th>Episode Count</th>
<th>Episode Length</th>
<th>Show Type</th>
<th>Age Rating</th>
<th>Genres</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<?php include __DIR__ . '/list-item.php' ?>
<?php endforeach ?>
</tbody>
</table>
<?php endforeach; ?>
</div>
</div> </div>
<?php endif ?> <?php endif ?>
</main> </main>

View File

@ -5,6 +5,9 @@
<?php if (empty($sections)): ?> <?php if (empty($sections)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<br />
<label>Filter: <input type='text' class='media-filter' /></label>
<br />
<?php foreach ($sections as $name => $items): ?> <?php foreach ($sections as $name => $items): ?>
<?php if (empty($items)): ?> <?php if (empty($items)): ?>
<section class="status"> <section class="status">

View File

@ -5,12 +5,15 @@
<?php if (empty($sections)): ?> <?php if (empty($sections)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<br />
<label>Filter: <input type='text' class='media-filter' /></label>
<br />
<?php foreach ($sections as $name => $items): ?> <?php foreach ($sections as $name => $items): ?>
<h2><?= $name ?></h2> <h2><?= $name ?></h2>
<?php if (empty($items)): ?> <?php if (empty($items)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<table> <table class='media-wrap'>
<thead> <thead>
<tr> <tr>
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>

View File

@ -23,7 +23,6 @@ try
'refresh:thumbnails' => Command\UpdateThumbnails::class, 'refresh:thumbnails' => Command\UpdateThumbnails::class,
'regenerate-thumbnails' => Command\UpdateThumbnails::class, 'regenerate-thumbnails' => Command\UpdateThumbnails::class,
'lists:sync' => Command\SyncLists::class, 'lists:sync' => Command\SyncLists::class,
'mal_id:check' => Command\MALIDCheck::class,
]))->run(); ]))->run();
} }
catch (\Exception $e) catch (\Exception $e)

View File

@ -7,18 +7,19 @@ sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,eve
"GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data= "GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=
JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)}); JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)});
AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect(); AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)});AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=[];data.forEach(function(x){var item= var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});AnimeClient.on(".media-filter","input",function(event){var rawFilter=event.target.value;var filter=new RegExp(rawFilter,"i");if(rawFilter!==""){AnimeClient.$("article.media").forEach(function(article){var titleLink=AnimeClient.$(".name a",article)[0];var title=String(titleLink.textContent).trim();if(!filter.test(title))AnimeClient.hide(article);else AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){var titleCell=
x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" class="mal-check" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+ AnimeClient.$("td.align-left",tr)[0];var titleLink=AnimeClient.$("a",titleCell)[0];var linkTitle=String(titleLink.textContent).trim();var textTitle=String(titleCell.textContent).trim();if(!(filter.test(linkTitle)||filter.test(textTitle)))AnimeClient.hide(tr);else AnimeClient.show(tr)})}else{AnimeClient.$("article.media").forEach(function(article){return AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){return AnimeClient.show(tr)})}});if("serviceWorker"in
x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/anime/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/anime/details/'+ navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)});AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+
item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+ (current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" class="mal-check" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+
item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+ x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/anime/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/anime/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});
'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/manga/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}var search=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query}, return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+
function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel= item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/manga/details/'+
AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:watchedCount+1}};if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status= item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}var search=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};
"completed";AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/anime/increment"),{data:data,dataType:"json",type:"POST",success:function(res){var resData=JSON.parse(res);if(resData.errors){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]); if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","input",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel=AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=
AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop()}})});var search$1=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults, AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:watchedCount+1}};if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status="completed";AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/anime/increment"),{data:data,dataType:"json",type:"POST",success:function(res){var resData=JSON.parse(res);if(resData.errors){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);
status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search$1(query)}));AnimeClient.on(".manga.list","click",".edit-buttons button",function(e){var thisSel=e.target;var parentSel= AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+
AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,10);var mangaName=AnimeClient.$(".name",parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)|| title+". ");AnimeClient.scrollToTop()}})});var search$1=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","input",
completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",type:"POST",mimeType:"application/json",success:function(){if(data.data.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.$("."+type+"s_read",parentSel)[0].textContent= AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search$1(query)}));AnimeClient.on(".manga.list","click",".edit-buttons button",function(e){var thisSel=e.target;var parentSel=AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,
completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})(); 10);var mangaName=AnimeClient.$(".name",parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)||completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",type:"POST",
mimeType:"application/json",success:function(){if(data.data.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})();
//# sourceMappingURL=scripts-authed.min.js.map //# sourceMappingURL=scripts-authed.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -7,5 +7,7 @@ sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,eve
"GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data= "GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=
JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)}); JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)});
AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect(); AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)})})(); var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});AnimeClient.on(".media-filter","input",function(event){var rawFilter=event.target.value;var filter=new RegExp(rawFilter,"i");if(rawFilter!==""){AnimeClient.$("article.media").forEach(function(article){var titleLink=AnimeClient.$(".name a",article)[0];var title=String(titleLink.textContent).trim();if(!filter.test(title))AnimeClient.hide(article);else AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){var titleCell=
AnimeClient.$("td.align-left",tr)[0];var titleLink=AnimeClient.$("a",titleCell)[0];var linkTitle=String(titleLink.textContent).trim();var textTitle=String(titleCell.textContent).trim();if(!(filter.test(linkTitle)||filter.test(textTitle)))AnimeClient.hide(tr);else AnimeClient.show(tr)})}else{AnimeClient.$("article.media").forEach(function(article){return AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){return AnimeClient.show(tr)})}});if("serviceWorker"in
navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)})})();
//# sourceMappingURL=scripts.min.js.map //# sourceMappingURL=scripts.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@ const search = (query) => {
}; };
if (_.hasElement('.anime #search')) { if (_.hasElement('.anime #search')) {
_.on('#search', 'keyup', _.throttle(250, (e) => { _.on('#search', 'input', _.throttle(250, (e) => {
const query = encodeURIComponent(e.target.value); const query = encodeURIComponent(e.target.value);
if (query === '') { if (query === '') {
return; return;

View File

@ -36,3 +36,40 @@ _.on('.js-clear-cache', 'click', () => {
behavior: 'smooth', behavior: 'smooth',
}); });
}); });
// Filter the current page (cover view)
_.on('.media-filter', 'input', (event) => {
const rawFilter = event.target.value;
const filter = new RegExp(rawFilter, 'i');
// console.log('Filtering items by: ', filter);
if (rawFilter !== '') {
// Filter the cover view
_.$('article.media').forEach(article => {
const titleLink = _.$('.name a', article)[0];
const title = String(titleLink.textContent).trim();
if ( ! filter.test(title)) {
_.hide(article);
} else {
_.show(article);
}
});
// Filter the list view
_.$('table.media-wrap tbody tr').forEach(tr => {
const titleCell = _.$('td.align-left', tr)[0];
const titleLink = _.$('a', titleCell)[0];
const linkTitle = String(titleLink.textContent).trim();
const textTitle = String(titleCell.textContent).trim();
if ( ! (filter.test(linkTitle) || filter.test(textTitle))) {
_.hide(tr);
} else {
_.show(tr);
}
});
} else {
_.$('article.media').forEach(article => _.show(article));
_.$('table.media-wrap tbody tr').forEach(tr => _.show(tr));
}
});

View File

@ -11,7 +11,7 @@ const search = (query) => {
}; };
if (_.hasElement('.manga #search')) { if (_.hasElement('.manga #search')) {
_.on('#search', 'keyup', _.throttle(250, (e) => { _.on('#search', 'input', _.throttle(250, (e) => {
let query = encodeURIComponent(e.target.value); let query = encodeURIComponent(e.target.value);
if (query === '') { if (query === '') {
return; return;

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
class MissingIdException extends \InvalidArgumentException {}

View File

@ -100,7 +100,7 @@ final class Model
$config = $this->container->get('config'); $config = $this->container->get('config');
$anilistUser = $config->get(['anilist', 'username']); $anilistUser = $config->get(['anilist', 'username']);
if ( ! \is_string($anilistUser)) if ( ! (is_string($anilistUser) && $anilistUser !== ''))
{ {
throw new InvalidArgumentException('Anilist username is not defined in config'); throw new InvalidArgumentException('Anilist username is not defined in config');
} }
@ -124,10 +124,10 @@ final class Model
$mediaId = $this->getMediaIdFromMalId($data['mal_id'], mb_strtoupper($type)); $mediaId = $this->getMediaIdFromMalId($data['mal_id'], mb_strtoupper($type));
if (empty($mediaId)) /* if (empty($mediaId))
{ {
throw new InvalidArgumentException('Media id missing'); throw new InvalidArgumentException('Media id missing');
} } */
if ($type === 'ANIME') if ($type === 'ANIME')
{ {
@ -151,12 +151,18 @@ final class Model
* Create a list item with all the relevant data * Create a list item with all the relevant data
* *
* @param array $data * @param array $data
* @param string $type
* @return Request * @return Request
*/ */
public function createFullListItem(array $data): Request public function createFullListItem(array $data, string $type): Request
{ {
$createData = $data['data']; $createData = $data['data'];
$mediaId = $this->getMediaIdFromMalId($data['mal_id']); $mediaId = $this->getMediaIdFromMalId($data['mal_id'], strtoupper($type));
if (empty($mediaId))
{
throw new MissingIdException('No id mapping found');
}
$createData['id'] = $mediaId; $createData['id'] = $mediaId;
@ -213,6 +219,7 @@ final class Model
* Remove a list item * Remove a list item
* *
* @param string $malId - The id of the list item to remove * @param string $malId - The id of the list item to remove
* @param string $type - Them media type (anime/manga)
* @return Request * @return Request
*/ */
public function deleteListItem(string $malId, string $type): Request public function deleteListItem(string $malId, string $type): Request

View File

@ -1,241 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
use const Aviat\AnimeClient\SRC_DIR;
use function Amp\Promise\wait;
use Aviat\AnimeClient\API\{
APIRequestBuilder,
JsonAPI,
ParallelAPIRequest
};
use Aviat\Ion\Json;
final class MALIDCheck extends BaseCommand {
private $kitsuModel;
/**
* Check MAL mapping validity
*
* @param array $args
* @param array $options
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
* @throws \Throwable
*/
public function execute(array $args, array $options = []): void
{
$this->setContainer($this->setupContainer());
$this->setCache($this->container->get('cache'));
$this->kitsuModel = $this->container->get('kitsu-model');
$kitsuAnimeIdList = $this->formatKitsuList('anime');
$animeCount = count($kitsuAnimeIdList);
$this->echoBox("{$animeCount} mappings for Anime");
$animeMappings = $this->checkMALIds($kitsuAnimeIdList, 'anime');
$this->mappingStatus($animeMappings, $animeCount, 'anime');
$kitsuMangaIdList = $this->formatKitsuList('manga');
$mangaCount = count($kitsuMangaIdList);
$this->echoBox("{$mangaCount} mappings for Manga");
$mangaMappings = $this->checkMALIds($kitsuMangaIdList, 'manga');
$this->mappingStatus($mangaMappings, $mangaCount, 'manga');
$publicDir = realpath(SRC_DIR . '/../public') . '/';
file_put_contents($publicDir . 'mal_mappings.json', Json::encode([
'anime' => $animeMappings,
'manga' => $mangaMappings,
]));
$this->echoBox('Mapping file saved to "' . $publicDir . 'mal_mappings.json' . '"');
}
/**
* Format a kitsu list for the sake of comparision
*
* @param string $type
* @return array
*/
private function formatKitsuList(string $type = 'anime'): array
{
$options = [
'include' => 'media,media.mappings',
];
$data = $this->kitsuModel->{'getFullRaw' . ucfirst($type) . 'List'}($options);
if (empty($data))
{
return [];
}
$includes = JsonAPI::organizeIncludes($data['included']);
// Only bother with mappings from MAL that are of the specified media type
$includes['mappings'] = array_filter($includes['mappings'], function ($mapping) use ($type) {
return $mapping['externalSite'] === "myanimelist/{$type}";
});
$output = [];
foreach ($data['data'] as $listItem)
{
$id = $listItem['relationships']['media']['data']['id'];
$mediaItem = $includes[$type][$id];
// Set titles
$listItem['titles'] = $mediaItem['titles'];
$potentialMappings = $mediaItem['relationships']['mappings'];
$malId = NULL;
foreach ($potentialMappings as $mappingId)
{
if (array_key_exists($mappingId, $includes['mappings']))
{
$malId = $includes['mappings'][$mappingId]['externalId'];
}
}
// Skip to the next item if there isn't a MAL ID
if ($malId === NULL)
{
continue;
}
// Group by malIds to simplify lookup of media details
// for checking validity of the malId mappings
$output[$malId] = $listItem;
}
ksort($output);
return $output;
}
/**
* Check for valid Kitsu -> MAL mapping
*
* @param array $kitsuList
* @param string $type
* @return array
* @throws \Throwable
*/
private function checkMALIds(array $kitsuList, string $type): array
{
$goodMappings = [];
$badMappings = [];
$suspectMappings = [];
$responses = $this->makeMALRequests(array_keys($kitsuList), $type);
// If the page returns a 404, put it in the bad mappings list
// otherwise, do a search against the titles, to see if the mapping
// seems valid
foreach($responses as $id => $response)
{
$body = wait($response->getBody());
$titles = $kitsuList[$id]['titles'];
if ($response->getStatus() === 404)
{
$badMappings[$id] = $titles;
}
else
{
$titleMatches = FALSE;
// Attempt to determine if the id matches
// By searching for a matching title
foreach($titles as $title)
{
if (empty($title))
{
continue;
}
if (mb_stripos($body, $title) !== FALSE)
{
$titleMatches = TRUE;
$goodMappings[$id] = $title;
// Continue on outer loop
continue 2;
}
}
if ( ! $titleMatches)
{
$suspectMappings[$id] = $titles;
}
else
{
$goodMappings[$id] = $titles;
}
}
}
return [
'good' => $goodMappings,
'bad' => $badMappings,
'suspect' => $suspectMappings,
];
}
private function makeMALRequests(array $ids, string $type): array
{
$baseUrl = "https://myanimelist.net/{$type}/";
$requestChunks = array_chunk($ids, 50, TRUE);
$responses = [];
// Chunk parallel requests so that we don't hit rate
// limiting, and get spurious 404 HTML responses
foreach($requestChunks as $idChunk)
{
$requester = new ParallelAPIRequest();
foreach($idChunk as $id)
{
$request = APIRequestBuilder::simpleRequest($baseUrl . $id);
$requester->addRequest($request, (string)$id);
}
foreach($requester->getResponses() as $id => $response)
{
$responses[$id] = $response;
}
}
return $responses;
}
private function mappingStatus(array $mapping, int $count, string $type): void
{
$good = count($mapping['good']);
$bad = count($mapping['bad']);
$suspect = count($mapping['suspect']);
$uType = ucfirst($type);
$this->echoBox("{$uType} mappings: {$good}/{$count} Good, {$suspect}/{$count} Suspect, {$bad}/{$count} Broken");
}
}

View File

@ -16,11 +16,8 @@
namespace Aviat\AnimeClient\Command; namespace Aviat\AnimeClient\Command;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\
FailedResponseException, {Anilist\MissingIdException, FailedResponseException, JsonAPI, ParallelAPIRequest};
JsonAPI,
ParallelAPIRequest
};
use Aviat\AnimeClient\API\Anilist\Transformer\{ use Aviat\AnimeClient\API\Anilist\Transformer\{
AnimeListTransformer as AALT, AnimeListTransformer as AALT,
MangaListTransformer as AMLT MangaListTransformer as AMLT
@ -311,20 +308,11 @@ final class SyncLists extends BaseCommand {
$anilistUpdateItems = []; $anilistUpdateItems = [];
$kitsuUpdateItems = []; $kitsuUpdateItems = [];
$malBlackList = ($type === 'anime')
? [
27821, // Fate/stay night: Unlimited Blade Works - Prologue
29317, // Saekano: How to Raise a Boring Girlfriend Prologue
30514, // Nisekoinogatari
] : [
114638, // Cells at Work: Black
];
$malIds = array_keys($anilistList); $malIds = array_keys($anilistList);
$kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId')); $kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId'));
$missingMalIds = array_diff($malIds, $kitsuMalIds); $missingMalIds = array_diff($malIds, $kitsuMalIds);
$missingMalIds = array_diff($missingMalIds, $malBlackList);
// Add items on Anilist, but not Kitsu to Kitsu
foreach($missingMalIds as $mid) foreach($missingMalIds as $mid)
{ {
$itemsToAddToKitsu[] = array_merge($anilistList[$mid]['data'], [ $itemsToAddToKitsu[] = array_merge($anilistList[$mid]['data'], [
@ -337,11 +325,6 @@ final class SyncLists extends BaseCommand {
{ {
$malId = $kitsuItem['malId']; $malId = $kitsuItem['malId'];
if (\in_array((int)$malId, $malBlackList, TRUE))
{
continue;
}
if (array_key_exists($malId, $anilistList)) if (array_key_exists($malId, $anilistList))
{ {
$anilistItem = $anilistList[$malId]; $anilistItem = $anilistList[$malId];
@ -628,13 +611,25 @@ final class SyncLists extends BaseCommand {
{ {
$verb = ($action === 'update') ? 'updated' : 'created'; $verb = ($action === 'update') ? 'updated' : 'created';
$this->echoBox("Successfully {$verb} Kitsu {$type} list item with id: {$id}"); $this->echoBox("Successfully {$verb} Kitsu {$type} list item with id: {$id}");
continue;
} }
else
// Show a different message when you have an episode count mismatch
if (isset($responseData['errors'][0]['title']))
{ {
dump($responseData); $errorTitle = $responseData['errors'][0]['title'];
$verb = ($action === 'update') ? 'update' : 'create';
$this->echoBox("Failed to {$verb} Kitsu {$type} list item with id: {$id}"); if ($errorTitle === 'cannot exceed length of media')
{
$this->echoBox("Skipped Kitsu {$type} {$id} due to episode count mismatch with other API");
continue;
}
} }
dump($responseData);
$verb = ($action === 'update') ? 'update' : 'create';
$this->echoBox("Failed to {$verb} Kitsu {$type} list item with id: {$id}");
} }
} }
@ -660,7 +655,16 @@ final class SyncLists extends BaseCommand {
} }
else if ($action === 'create') else if ($action === 'create')
{ {
$requester->addRequest($this->anilistModel->createFullListItem($item, $type)); try
{
$requester->addRequest($this->anilistModel->createFullListItem($item, $type));
}
catch (MissingIdException $e)
{
// Case where there's a MAL mapping from Kitsu, but no equivalent Anlist item
$id = $item['mal_id'];
$this->echoBox("Skipping Anilist ${type} with mal_id: {$id} due to missing mapping");
}
} }
} }

View File

@ -61,6 +61,11 @@ final class AnimeCollection extends BaseController {
]); ]);
} }
public function index(): void
{
$this->redirect('/anime-collection/view', 303);
}
/** /**
* Search for anime * Search for anime
* *
@ -83,7 +88,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @return void * @return void
*/ */
public function index($view): void public function view($view): void
{ {
$viewMap = [ $viewMap = [
'' => 'cover', '' => 'cover',
@ -145,13 +150,21 @@ final class AnimeCollection extends BaseController {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
if (array_key_exists('hummingbird_id', $data)) if (array_key_exists('hummingbird_id', $data))
{ {
// @TODO verify data was updated correctly
$this->animeCollectionModel->update($data); $this->animeCollectionModel->update($data);
$this->setFlashMessage('Successfully updated collection item.', 'success');
// Verify the item was actually updated
if ($this->animeCollectionModel->wasUpdated($data))
{
$this->setFlashMessage('Successfully updated collection item.', 'success');
}
else
{
$this->setFlashMessage('Failed to update collection item.', 'error');
}
} }
else else
{ {
$this->setFlashMessage('Failed to update collection item', 'error'); $this->setFlashMessage('No item id to update. Update failed.', 'error');
} }
$this->sessionRedirect(); $this->sessionRedirect();
@ -175,21 +188,26 @@ final class AnimeCollection extends BaseController {
// Check for existing entry // Check for existing entry
if ($this->animeCollectionModel->get($data['id']) !== FALSE) if ($this->animeCollectionModel->get($data['id']) !== FALSE)
{ {
$this->setFlashMessage('Anime already exists, can not create duplicate', 'info'); // Redirect to the edit screen, because that's probably what you want!
$this->setFlashMessage('Anime already exists, update instead.', 'info');
$this->redirect("/anime-collection/edit/{$data['id']}", 303);
return;
} }
else
$this->animeCollectionModel->add($data);
// Verify the item was added
if ($this->animeCollectionModel->wasAdded($data))
{ {
// @TODO actually verify that collection item was added
$this->animeCollectionModel->add($data);
$this->setFlashMessage('Successfully added collection item', 'success'); $this->setFlashMessage('Successfully added collection item', 'success');
$this->sessionRedirect();
} }
} }
else else
{ {
$this->setFlashMessage('Failed to add collection item.', 'error'); $this->setFlashMessage('Failed to add collection item.', 'error');
$this->redirect('/anime-collection/add', 303);
} }
$this->sessionRedirect();
} }
/** /**
@ -204,12 +222,21 @@ final class AnimeCollection extends BaseController {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
if ( ! array_key_exists('hummingbird_id', $data)) if ( ! array_key_exists('hummingbird_id', $data))
{ {
$this->setFlashMessage("Can't delete item that doesn't exist", 'error');
$this->redirect('/anime-collection/view', 303); $this->redirect('/anime-collection/view', 303);
} }
// @TODO verify that item was actually deleted
$this->animeCollectionModel->delete($data); $this->animeCollectionModel->delete($data);
$this->setFlashMessage('Successfully removed anime from collection.', 'success');
// Verify that item was actually deleted
if ($this->animeCollectionModel->wasDeleted($data))
{
$this->setFlashMessage('Successfully removed anime from collection.', 'success');
}
else
{
$this->setFlashMessage('Failed to delete item from collection.', 'error');
}
$this->redirect('/anime-collection/view', 303); $this->redirect('/anime-collection/view', 303);
} }

View File

@ -166,7 +166,7 @@ class Anime extends API {
$requester = new ParallelAPIRequest(); $requester = new ParallelAPIRequest();
$requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu'); $requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu');
if (array_key_exists('mal_id', $data) && $this->anilistEnabled) if ($data['mal_id'] !== null && $this->anilistEnabled)
{ {
$requester->addRequest($this->anilistModel->createListItem($data, 'ANIME'), 'anilist'); $requester->addRequest($this->anilistModel->createListItem($data, 'ANIME'), 'anilist');
} }
@ -189,7 +189,7 @@ class Anime extends API {
$array = $data->toArray(); $array = $data->toArray();
if (array_key_exists('mal_id', $array) && $this->anilistEnabled) if ($array['mal_id'] !== null && $this->anilistEnabled)
{ {
$requester->addRequest($this->anilistModel->incrementListItem($data, 'ANIME'), 'anilist'); $requester->addRequest($this->anilistModel->incrementListItem($data, 'ANIME'), 'anilist');
} }
@ -218,7 +218,7 @@ class Anime extends API {
$array = $data->toArray(); $array = $data->toArray();
if (array_key_exists('mal_id', $array) && $this->anilistEnabled) if ($array['mal_id'] !== null && $this->anilistEnabled)
{ {
$requester->addRequest($this->anilistModel->updateListItem($data, 'ANIME'), 'anilist'); $requester->addRequest($this->anilistModel->updateListItem($data, 'ANIME'), 'anilist');
} }

View File

@ -140,7 +140,7 @@ final class AnimeCollection extends Collection {
// Check that the anime doesn't already exist // Check that the anime doesn't already exist
$existing = $this->get($id); $existing = $this->get($id);
if ($existing === FALSE) if ( ! empty($existing))
{ {
return; return;
} }
@ -160,7 +160,20 @@ final class AnimeCollection extends Collection {
'notes' => $data['notes'] 'notes' => $data['notes']
])->insert('anime_set'); ])->insert('anime_set');
$this->updateGenre($data['id']); $this->updateGenre($id);
}
/**
* Verify that an item was added
*
* @param $data
* @return bool
*/
public function wasAdded($data): bool
{
$row = $this->get($data['id']);
return ! empty($row);
} }
/** /**
@ -183,6 +196,30 @@ final class AnimeCollection extends Collection {
$this->db->set($data) $this->db->set($data)
->where('hummingbird_id', $id) ->where('hummingbird_id', $id)
->update('anime_set'); ->update('anime_set');
// Just in case, also update genres
$this->updateGenre($id);
}
/**
* Verify that the collection item was updated
*
* @param $data
* @return bool
*/
public function wasUpdated($data): bool
{
$row = $this->get($data['hummingbird_id']);
foreach ($data as $key => $value)
{
if ((string)$row[$key] !== (string)$value)
{
return FALSE;
}
}
return TRUE;
} }
/** /**
@ -206,6 +243,13 @@ final class AnimeCollection extends Collection {
->delete('anime_set'); ->delete('anime_set');
} }
public function wasDeleted($data): bool
{
$animeRow = $this->get($data['hummingbird_id']);
return empty($animeRow);
}
/** /**
* Get the details of a collection item * Get the details of a collection item
* *
@ -264,7 +308,8 @@ final class AnimeCollection extends Collection {
if (array_key_exists($id, $output)) if (array_key_exists($id, $output))
{ {
$output[$id][] = $genre; $output[$id][] = $genre;
} else }
else
{ {
$output[$id] = [$genre]; $output[$id] = [$genre];
} }
@ -272,6 +317,8 @@ final class AnimeCollection extends Collection {
} }
catch (PDOException $e) {} catch (PDOException $e) {}
$this->db->reset_query();
return $output; return $output;
} }
@ -283,44 +330,67 @@ final class AnimeCollection extends Collection {
*/ */
private function updateGenre($animeId): void private function updateGenre($animeId): void
{ {
// Get api information
$anime = $this->animeModel->getAnimeById($animeId);
$this->addNewGenres($anime['genres']);
$genreInfo = $this->getGenreData(); $genreInfo = $this->getGenreData();
$genres = $genreInfo['genres']; $genres = $genreInfo['genres'];
$links = $genreInfo['links']; $links = $genreInfo['links'];
// Get api information $linksToInsert = [];
$anime = $this->animeModel->getAnimeById($animeId);
foreach ($anime['genres'] as $genre) foreach ($anime['genres'] as $animeGenre)
{ {
// Add genres that don't currently exist
if ( ! \in_array($genre, $genres, TRUE))
{
$this->db->set('genre', $genre)
->insert('genres');
$genres[] = $genre;
}
// Update link table // Update link table
// Get id of genre to put in link table // Get id of genre to put in link table
$flippedGenres = array_flip($genres); $flippedGenres = array_flip($genres);
$genreId = $flippedGenres[$animeGenre];
$insertArray = [ $animeLinks = $links[$animeId] ?? [];
'hummingbird_id' => $animeId,
'genre_id' => $flippedGenres[$genre] if ( ! \in_array($flippedGenres[$animeGenre], $animeLinks, TRUE))
{
$linksToInsert[] = [
'hummingbird_id' => $animeId,
'genre_id' => $genreId,
];
}
}
if ( ! empty($linksToInsert))
{
$this->db->insertBatch('genre_anime_set_link', $linksToInsert);
}
}
/**
* Add genres to the database
*
* @param array $genres
*/
private function addNewGenres(array $genres): void
{
$existingGenres = $this->getExistingGenres();
$newGenres = array_diff($genres, $existingGenres);
$insert = [];
foreach ($newGenres as $genre)
{
$insert[] = [
'genre' => $genre,
]; ];
}
if (array_key_exists($animeId, $links)) try
{ {
if ( ! \in_array($flippedGenres[$genre], $links[$animeId], TRUE)) $this->db->insert_batch('genres', $insert);
{ }
$this->db->set($insertArray)->insert('genre_anime_set_link'); catch (PDOException $e)
} {
} dump($e);
else
{
$this->db->set($insertArray)->insert('genre_anime_set_link');
}
} }
} }
@ -345,11 +415,14 @@ final class AnimeCollection extends Collection {
$query = $this->db->select('id, genre') $query = $this->db->select('id, genre')
->from('genres') ->from('genres')
->get(); ->get();
foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $genre) foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $genre)
{ {
$genres[$genre['id']] = $genre['genre']; $genres[$genre['id']] = $genre['genre'];
} }
$this->db->reset_query();
return $genres; return $genres;
} }
@ -360,6 +433,7 @@ final class AnimeCollection extends Collection {
$query = $this->db->select('hummingbird_id, genre_id') $query = $this->db->select('hummingbird_id, genre_id')
->from('genre_anime_set_link') ->from('genre_anime_set_link')
->get(); ->get();
foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $link) foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $link)
{ {
if (array_key_exists($link['hummingbird_id'], $links)) if (array_key_exists($link['hummingbird_id'], $links))
@ -371,6 +445,8 @@ final class AnimeCollection extends Collection {
} }
} }
$this->db->reset_query();
return $links; return $links;
} }
} }

View File

@ -19,6 +19,8 @@ namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use PDOException; use PDOException;
use function Query;
/** /**
* Base model for anime and manga collections * Base model for anime and manga collections
*/ */
@ -47,7 +49,7 @@ class Collection extends DB {
try try
{ {
$this->db = \Query($this->dbConfig); $this->db = Query($this->dbConfig);
$this->validDatabase = TRUE; $this->validDatabase = TRUE;
} }
catch (PDOException $e) {} catch (PDOException $e) {}

View File

@ -61,7 +61,6 @@ const SETTINGS_MAP = [
'type' => 'string', 'type' => 'string',
'title' => 'Anilist Username', 'title' => 'Anilist Username',
'default' => '', 'default' => '',
'readonly' => TRUE,
'description' => 'Login username for Anilist account to integrate with', 'description' => 'Login username for Anilist account to integrate with',
], ],
'access_token' => [ 'access_token' => [