diff --git a/lib/Pooru/API/V0.pm b/lib/Pooru/API/V0.pm index f8c2744..3b22fda 100644 --- a/lib/Pooru/API/V0.pm +++ b/lib/Pooru/API/V0.pm @@ -5,6 +5,18 @@ use DBD::SQLite::Constants ":dbd_sqlite_string_mode"; use DBIx::Migration; use Pooru::Schema; +use Pooru::API::V0::Model::Media; +use Pooru::API::V0::Model::Tags; + +use DBI; + +use constant { + "Pooru::API::V0::Model::Media::ROWS" => 12, + "Pooru::API::V0::Model::Media::SIMILAR_ROWS" => 6, + "Pooru::API::V0::Model::Tags::ROWS" => 100, + "Pooru::API::V0::Model::Tags::SEARCH_ROWS" => 15, +}; + sub _search_opts () { return { @@ -63,6 +75,32 @@ sub startup ($self) dir => $self->app->home->child("migrations")->to_string, })->migrate; + $self->helper(dbh => sub { + state $dbh = DBI->connect( + $config->{dsn}, + $config->{db_username}, + $config->{db_password}, + { + PrintError => 0, + RaiseError => 1, + AutoCommit => 1, + ($config->{dbi_connect_args} // {})->%*, + } + ) + }); + + $self->dbh->{FetchHashKeyName} = "NAME_lc"; + if ($self->dbh->{Driver}->{Name} eq "SQLite") { + $self->dbh->do("PRAGMA foreign_keys = ON"); + } + + $self->helper(tags_model => sub { + state $model = Pooru::API::V0::Model::Tags->new($self->dbh) + }); + $self->helper(media_model => sub { + state $model = Pooru::API::V0::Model::Media->new($self->dbh) + }); + my $r = $self->routes; $r->get("/meta")->to("meta#index")->name("meta"); diff --git a/lib/Pooru/API/V0/Controller/Media.pm b/lib/Pooru/API/V0/Controller/Media.pm index 9703514..07a0627 100644 --- a/lib/Pooru/API/V0/Controller/Media.pm +++ b/lib/Pooru/API/V0/Controller/Media.pm @@ -7,52 +7,26 @@ my %search_opts = Pooru::API::V0::_search_opts->%*; sub _list_no_tags ($self, $page) { - my $paged_media = $self->schema->resultset("Media") - ->search(undef, $search_opts{Media}) - ->page($page); - my @media = map +{ - id => $_->id, - storage_id => $_->storage_id, - filename => $_->filename, - content_type => $_->content_type, - upload_datetime => $_->upload_datetime, - }, $paged_media->all; + my $paged_media = $self->media_model->get->page($page); return $self->render(json => { - media => [@media], - pager => {$self->pager($paged_media)}, + media => $paged_media->fetchall, + pager => $paged_media->pager, }); } -# Executes -# -# SELECT * FROM tagged_media_view -# WHERE (tag_display = t_1 OR ... OR tag_display = t_N) -# GROUP BY media_id HAVING COUNT(media_id) = N sub _list_with_tags ($self, $page, @tags) { - my $attrs = { - $search_opts{TaggedMediaView_media}->%*, - - # Use "0 + ?" as otherwise a bare "?" interprets the value as - # TEXT, breaking the functionality. - having => \["COUNT(media_id) = 0 + ?", scalar @tags], - group_by => "media_id", - }; - - my $paged_media = $self->schema->resultset("TaggedMediaView") - ->search({tag_display => [@tags]}, $attrs)->page($page); - my @media = map +{ - id => $_->media_id, - storage_id => $_->media_storage_id, - filename => $_->media_filename, - content_type => $_->media_content_type, - upload_datetime => $_->media_upload_datetime, - }, $paged_media->all; + my $paged_media_ids = $self->media_model->with_all_tags(@tags) + ->page($page); + my @media_ids = map {$_->{media_id}} $paged_media_ids->fetchall->@*; return $self->render(json => { - media => [@media], - pager => {$self->pager($paged_media)}, + media => $self->media_model->get(@media_ids)->page($page) + ->fetchall, + # Use the original pager, as the one for the media is already + # capped at the size of a single page. + pager => $paged_media_ids->pager, }); } @@ -72,10 +46,18 @@ sub list ($self) status => 400, ) if $v->has_error; + return $self->render( + json => {error => "Too many tags."}, + status => 400, + ) if @tags > Pooru::API::V0::Model::Tags::ROWS; + + my @tag_ids; + @tag_ids = map {$_->{id}} $self->tags_model + ->get(display => [@tags])->fetchall->@* if @tags > 0; + return @tags == 0 ? $self->_list_no_tags($page) : - $self->_list_with_tags($page, @tags); - + $self->_list_with_tags($page, @tag_ids); } sub show ($self) @@ -88,85 +70,45 @@ sub show ($self) status => 400, ) if $v->has_error; - my $media_id = $self->stash("media_id"); - my $media = $self->schema->resultset("Media") - ->single({id => $media_id}); - + my $media = $self->media_model->get($self->stash("media_id"))->single; return $self->render( json => {error => "Media not found"}, status => 404, ) if !defined($media); - my $paged_tags = $self->schema->resultset("TaggedMediaView") - ->search({media_id => $media_id}, - $search_opts{TaggedMediaView_tags})->page($page); - my @tags = map +{ - id => $_->tag_id, - name => $_->tag_name, - kind_id => $_->tag_kind_id, - display => $_->tag_display, - count => $_->tag_count, - }, $paged_tags->all; + my $paged_tags = $self->tags_model + ->ranked_for_media($media->{id})->page($page); return $self->render(json => { - id => $media->id, - storage_id => $media->storage_id, - filename => $media->filename, - content_type => $media->content_type, - upload_datetime => $media->upload_datetime, - tags => [@tags], - pager => {$self->pager($paged_tags)}, + $media->%*, + tags => $paged_tags->fetchall, + pager => $paged_tags->pager, }); } -# Executes -# SELECT *, COUNT(tag_id) AS similarity_score FROM tagged_media_view -# WHERE tag_id IN ( -# SELECT tag_id FROM tagged_media_view WHERE media_id = m -# ) AND media_id != m -# GROUP BY media_id ORDER BY similarity_score sub similar ($self) { - my $media_id = $self->stash("media_id"); - my $media = $self->schema->resultset("Media") - ->single({id => $media_id}); - + my $media = $self->media_model->get($self->stash("media_id"))->single; return $self->render( json => {error => "Media not found"}, status => 404, ) if !defined($media); - my %attrs = ( - select => ["tag_id"], - rows => 100, - ); - my $tags = $self->schema->resultset("MediaTag") - ->search({media_id => $media_id}, \%attrs)->as_query; + my @similar_media = $self->media_model + ->similar($media->{id})->fetchall->@*; + my %media = map +( + $_->{id} => $_ + ), $self->media_model + ->get(map {$_->{media_id}} @similar_media)->fetchall->@*; - my %search = ( - media_id => {"!=", $media_id}, - tag_id => {"-in", $tags}, - ); - %attrs = ( - "+select" => [{count => "tag_id", -as => "similarity_score"}], - "+as" => ["similarity_score"], - group_by => "media_id", - order_by => {-desc => "similarity_score"}, - rows => 6, - ); - my @media = map +{ - id => $_->media_id, - storage_id => $_->media_storage_id, - filename => $_->media_filename, - content_type => $_->media_content_type, - upload_datetime => $_->media_upload_datetime, - similarity_score => $_->get_column("similarity_score"), - }, $self->schema->resultset("TaggedMediaView") - ->search(\%search, \%attrs)->all; + foreach my $sm (@similar_media) { + $sm = { + score => $sm->{score}, + $media{$sm->{media_id}}->%*, + } + } - return $self->render(json => { - media => [@media], - }); + return $self->render(json => {media => [@similar_media]}); } 1; diff --git a/lib/Pooru/API/V0/Controller/Random.pm b/lib/Pooru/API/V0/Controller/Random.pm index ec8c37d..2ac42fe 100644 --- a/lib/Pooru/API/V0/Controller/Random.pm +++ b/lib/Pooru/API/V0/Controller/Random.pm @@ -3,27 +3,27 @@ use Mojo::Base "Mojolicious::Controller", -signatures; sub media ($self) { - my $media = $self->schema->resultset("Media")->random->single; + my $media_id = $self->media_model->random_id; return $self->render( json => {error => "Media not found."}, status => 404, - ) if !defined($media); + ) if !defined($media_id); - return $self->redirect_to("show_media", media_id => $media->id); + return $self->redirect_to("show_media", media_id => $media_id); } sub tag ($self) { - my $tag = $self->schema->resultset("Tag")->random->single; + my $tag_id = $self->tags_model->random_id; return $self->render( json => {error => "Tag not found."}, status => 404, - ) if !defined($tag); + ) if !defined($tag_id); return $self->redirect_to( - $self->url_for("show_tag")->query(id => $tag->id)); + $self->url_for("show_tag")->query(id => $tag_id)); } 1; diff --git a/lib/Pooru/API/V0/Controller/Tags.pm b/lib/Pooru/API/V0/Controller/Tags.pm index c3c7b0e..0d1d0f4 100644 --- a/lib/Pooru/API/V0/Controller/Tags.pm +++ b/lib/Pooru/API/V0/Controller/Tags.pm @@ -13,19 +13,11 @@ sub list ($self) status => 400, ) if $v->has_error; - my $paged_tags = $self->schema->resultset("TagCountView") - ->search(undef, $search_opts{TagCountView})->page($page); - my @tags = map +{ - id => $_->id, - name => $_->name, - kind_id => $_->kind_id, - display => $_->display, - count => $_->count, - }, $paged_tags->all; + my $paged_tags = $self->tags_model->ranked->page($page); return $self->render(json => { - tags => [@tags], - pager => {$self->pager($paged_tags)}, + tags => $paged_tags->fetchall, + pager => $paged_tags->pager, }); } @@ -41,7 +33,7 @@ sub show ($self) status => 400, ) if $v->has_error; - my %search = ($v->topic => $tag_id_or_name); + my %search = ($v->topic => [$tag_id_or_name]); my $kind_id = $v->optional("kind_id")->param; return $self->render( @@ -50,13 +42,9 @@ sub show ($self) ) if $v->has_error; $search{kind_id} = $kind_id if defined($kind_id); - my @tags = map +{ - id => $_->id, - name => $_->name, - kind_id => $_->kind_id, - display => $_->display, - count => $_->count, - }, $self->schema->resultset("TagCountView")->search(\%search)->all; + my @tag_ids = map {$_->{id}} $self->tags_model + ->get(%search)->fetchall->@*; + my @tags = $self->tags_model->ranked_for_tags(@tag_ids)->fetchall->@*; return $self->render( json => {error => "Tag not found."}, @@ -83,19 +71,7 @@ sub search ($self) $q =~ s/[%\\_]/\\$&/g; $q .= '%'; - my %attrs = ( - $search_opts{TagCountView}->%*, - rows => 10, - ); - - my @tags = map +{ - id => $_->id, - name => $_->name, - kind_id => $_->kind_id, - display => $_->display, - count => $_->count, - }, $self->schema->resultset("TagCountView") - ->search({name => \["LIKE ? ESCAPE '\\'", $q]}, \%attrs); + my @tags = $self->tags_model->search($q)->fetchall->@*; return $self->render(json => {tags => [@tags]}); } diff --git a/pooru-api-v0.conf b/pooru-api-v0.conf index d6710e8..d23298b 100644 --- a/pooru-api-v0.conf +++ b/pooru-api-v0.conf @@ -1,5 +1,11 @@ +use DBD::SQLite::Constants ":dbd_sqlite_string_mode"; + { dsn => "dbi:SQLite:db/pooru.db", + dbi_connect_args => { + # Highly recommended to keep this. + sqlite_string_mode => DBD_SQLITE_STRING_MODE_UNICODE_STRICT, + }, secrets => [ # Generate with "openssl rand -hex 32".