diff --git a/app/Http/Controllers/Api/CardController.php b/app/Http/Controllers/Api/CardController.php index 87eb52d..e5578d2 100644 --- a/app/Http/Controllers/Api/CardController.php +++ b/app/Http/Controllers/Api/CardController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api; use App\Metadata; use App\MetadataResolver\MetadataResolver; +use App\Tag; use App\Utilities\Formatter; use Illuminate\Http\Request; @@ -41,6 +42,13 @@ class CardController 'image' => $resolved->image, 'expires_at' => $resolved->expires_at ]); + + $tagIds = []; + foreach ($resolved->tags as $tagName) { + $tag = Tag::firstOrCreate(['name' => $tagName]); + $tagIds[] = $tag->id; + } + $metadata->tags()->sync($tagIds); } $response = response($metadata); diff --git a/app/Listeners/LinkCollector.php b/app/Listeners/LinkCollector.php index 0354f65..1ca8682 100644 --- a/app/Listeners/LinkCollector.php +++ b/app/Listeners/LinkCollector.php @@ -5,6 +5,7 @@ namespace App\Listeners; use App\Events\LinkDiscovered; use App\Metadata; use App\MetadataResolver\MetadataResolver; +use App\Tag; use App\Utilities\Formatter; use GuzzleHttp\Exception\TransferException; use Illuminate\Contracts\Queue\ShouldQueue; @@ -47,12 +48,19 @@ class LinkCollector if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) { try { $resolved = $this->metadataResolver->resolve($url); - Metadata::updateOrCreate(['url' => $url], [ + $metadata = Metadata::updateOrCreate(['url' => $url], [ 'title' => $resolved->title, 'description' => $resolved->description, 'image' => $resolved->image, 'expires_at' => $resolved->expires_at ]); + + $tagIds = []; + foreach ($resolved->tags as $tagName) { + $tag = Tag::firstOrCreate(['name' => $tagName]); + $tagIds[] = $tag->id; + } + $metadata->tags()->sync($tagIds); } catch (TransferException $e) { // 何らかの通信エラーによってメタデータの取得に失敗した時、とりあえずエラーログにURLを残す Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url); diff --git a/app/Metadata.php b/app/Metadata.php index dc44009..d516b0b 100644 --- a/app/Metadata.php +++ b/app/Metadata.php @@ -14,4 +14,9 @@ class Metadata extends Model protected $visible = ['url', 'title', 'description', 'image', 'expires_at']; protected $dates = ['created_at', 'updated_at', 'expires_at']; + + public function tags() + { + return $this->belongsToMany(Tag::class)->withTimestamps(); + } } diff --git a/app/MetadataResolver/KomifloResolver.php b/app/MetadataResolver/KomifloResolver.php index c906548..7073015 100644 --- a/app/MetadataResolver/KomifloResolver.php +++ b/app/MetadataResolver/KomifloResolver.php @@ -34,6 +34,20 @@ class KomifloResolver implements Resolver ($json['content']['parents'][0]['data']['title'] ?? '?'); $metadata->image = 'https://t.komiflo.com/564_mobile_large_3x/' . $json['content']['named_imgs']['cover']['filename']; + // 作者情報 + if (!empty($json['content']['attributes']['artists']['children'])) { + foreach ($json['content']['attributes']['artists']['children'] as $artist) { + $metadata->tags[] = preg_replace('/\s/', '_', $artist['data']['name']); + } + } + + // タグ + if (!empty($json['content']['attributes']['tags']['children'])) { + foreach ($json['content']['attributes']['tags']['children'] as $tag) { + $metadata->tags[] = preg_replace('/\s/', '_', $tag['data']['name']); + } + } + return $metadata; } else { throw new \RuntimeException("{$res->getStatusCode()}: $url"); diff --git a/app/MetadataResolver/Metadata.php b/app/MetadataResolver/Metadata.php index dbe8654..62d16e8 100644 --- a/app/MetadataResolver/Metadata.php +++ b/app/MetadataResolver/Metadata.php @@ -6,9 +6,21 @@ use Carbon\Carbon; class Metadata { + /** @var string タイトル */ public $title = ''; + + /** @var string 概要 */ public $description = ''; + + /** @var string サムネイルのURL */ public $image = ''; - /** @var Carbon|null */ + + /** @var Carbon|null メタデータの有効期限 */ public $expires_at = null; + + /** + * @var string[] タグ + * チェックインタグと同様に保存されるため、スペースや改行文字を含めてはいけません。 + */ + public $tags = []; } diff --git a/app/MetadataResolver/MetadataResolver.php b/app/MetadataResolver/MetadataResolver.php index 565e05f..129899a 100644 --- a/app/MetadataResolver/MetadataResolver.php +++ b/app/MetadataResolver/MetadataResolver.php @@ -15,7 +15,7 @@ class MetadataResolver implements Resolver '~www\.melonbooks\.co\.jp/detail/detail\.php~' => MelonbooksResolver::class, '~ec\.toranoana\.(jp|shop)/(tora|joshi)(_[rd]+)?/(ec|digi)/item/~' => ToranoanaResolver::class, '~iwara\.tv/videos/.*~' => IwaraResolver::class, - '~www\.dlsite\.com/.*/work/=/product_id/..\d+\.html~' => DLsiteResolver::class, + '~www\.dlsite\.com/.*/work/=/product_id/..\d+(\.html)?~' => DLsiteResolver::class, '~dlsite\.jp/mawtw/..\d+~' => DLsiteResolver::class, '~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class, '~fantia\.jp/posts/\d+~' => FantiaResolver::class, diff --git a/app/MetadataResolver/PixivResolver.php b/app/MetadataResolver/PixivResolver.php index b42092b..69c9937 100644 --- a/app/MetadataResolver/PixivResolver.php +++ b/app/MetadataResolver/PixivResolver.php @@ -49,6 +49,43 @@ class PixivResolver implements Resolver return str_replace('i.pximg.net', 'i.pixiv.cat', $pixivUrl); } + /** + * HTMLからタグとして利用可能な情報を抽出する + * @param string $html ページ HTML + * @return string[] タグ + */ + public function extractTags(string $html): array + { + $dom = new \DOMDocument(); + @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); + $xpath = new \DOMXPath($dom); + + $nodes = $xpath->query("//meta[@name='keywords']"); + if ($nodes->length === 0) { + return []; + } + + $keywords = $nodes->item(0)->getAttribute('content'); + $tags = []; + + foreach (mb_split(',', $keywords) as $keyword) { + $keyword = trim($keyword); + + if (empty($keyword)) { + continue; + } + + // 一部の固定キーワードは無視 + if (array_search($keyword, ['R-18', 'イラスト', 'pixiv', 'ピクシブ'], true)) { + continue; + } + + $tags[] = preg_replace('/\s/', '_', $keyword); + } + + return $tags; + } + public function resolve(string $url): Metadata { parse_str(parse_url($url, PHP_URL_QUERY), $params); @@ -78,6 +115,8 @@ class PixivResolver implements Resolver $metadata->image = $this->proxize($illustUrl); + $metadata->tags = $this->extractTags($res->getBody()); + return $metadata; } else { throw new \RuntimeException("{$res->getStatusCode()}: $url"); diff --git a/database/migrations/2019_04_29_111019_create_metadata_tag_table.php b/database/migrations/2019_04_29_111019_create_metadata_tag_table.php new file mode 100644 index 0000000..fdef664 --- /dev/null +++ b/database/migrations/2019_04_29_111019_create_metadata_tag_table.php @@ -0,0 +1,33 @@ +increments('id'); + $table->text('metadata_url')->index(); + $table->integer('tag_id')->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('metadata_tag'); + } +} diff --git a/resources/assets/js/checkin.js b/resources/assets/js/checkin.js index bd06bfa..e68fc0c 100644 --- a/resources/assets/js/checkin.js +++ b/resources/assets/js/checkin.js @@ -33,9 +33,11 @@ $('#tagInput').on('keydown', function (ev) { case 'Tab': case 'Enter': case ' ': - insertTag($this.val().trim()); - $this.val(''); - updateTags(); + if (ev.originalEvent.isComposing !== true) { + insertTag($this.val().trim()); + $this.val(''); + updateTags(); + } ev.preventDefault(); break; } diff --git a/yarn.lock b/yarn.lock index 548e116..9629d93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4157,9 +4157,9 @@ isobject@^3.0.0, isobject@^3.0.1: integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= jquery@^3.2.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" - integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg== + version "3.4.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== js-cookie@^2.2.0: version "2.2.0"