api/v0: switch the API to the new model

This commit is contained in:
Lucas Gabriel Vuotto 2025-05-03 13:34:26 +00:00
parent 0635e37e13
commit 230c595e80
5 changed files with 99 additions and 137 deletions

View file

@ -5,6 +5,18 @@ use DBD::SQLite::Constants ":dbd_sqlite_string_mode";
use DBIx::Migration; use DBIx::Migration;
use Pooru::Schema; 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 () sub _search_opts ()
{ {
return { return {
@ -63,6 +75,32 @@ sub startup ($self)
dir => $self->app->home->child("migrations")->to_string, dir => $self->app->home->child("migrations")->to_string,
})->migrate; })->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; my $r = $self->routes;
$r->get("/meta")->to("meta#index")->name("meta"); $r->get("/meta")->to("meta#index")->name("meta");

View file

@ -7,52 +7,26 @@ my %search_opts = Pooru::API::V0::_search_opts->%*;
sub _list_no_tags ($self, $page) sub _list_no_tags ($self, $page)
{ {
my $paged_media = $self->schema->resultset("Media") my $paged_media = $self->media_model->get->page($page);
->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;
return $self->render(json => { return $self->render(json => {
media => [@media], media => $paged_media->fetchall,
pager => {$self->pager($paged_media)}, 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) sub _list_with_tags ($self, $page, @tags)
{ {
my $attrs = { my $paged_media_ids = $self->media_model->with_all_tags(@tags)
$search_opts{TaggedMediaView_media}->%*, ->page($page);
my @media_ids = map {$_->{media_id}} $paged_media_ids->fetchall->@*;
# 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;
return $self->render(json => { return $self->render(json => {
media => [@media], media => $self->media_model->get(@media_ids)->page($page)
pager => {$self->pager($paged_media)}, ->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, status => 400,
) if $v->has_error; ) 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 ? return @tags == 0 ?
$self->_list_no_tags($page) : $self->_list_no_tags($page) :
$self->_list_with_tags($page, @tags); $self->_list_with_tags($page, @tag_ids);
} }
sub show ($self) sub show ($self)
@ -88,85 +70,45 @@ sub show ($self)
status => 400, status => 400,
) if $v->has_error; ) if $v->has_error;
my $media_id = $self->stash("media_id"); my $media = $self->media_model->get($self->stash("media_id"))->single;
my $media = $self->schema->resultset("Media")
->single({id => $media_id});
return $self->render( return $self->render(
json => {error => "Media not found"}, json => {error => "Media not found"},
status => 404, status => 404,
) if !defined($media); ) if !defined($media);
my $paged_tags = $self->schema->resultset("TaggedMediaView") my $paged_tags = $self->tags_model
->search({media_id => $media_id}, ->ranked_for_media($media->{id})->page($page);
$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;
return $self->render(json => { return $self->render(json => {
id => $media->id, $media->%*,
storage_id => $media->storage_id, tags => $paged_tags->fetchall,
filename => $media->filename, pager => $paged_tags->pager,
content_type => $media->content_type,
upload_datetime => $media->upload_datetime,
tags => [@tags],
pager => {$self->pager($paged_tags)},
}); });
} }
# 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) sub similar ($self)
{ {
my $media_id = $self->stash("media_id"); my $media = $self->media_model->get($self->stash("media_id"))->single;
my $media = $self->schema->resultset("Media")
->single({id => $media_id});
return $self->render( return $self->render(
json => {error => "Media not found"}, json => {error => "Media not found"},
status => 404, status => 404,
) if !defined($media); ) if !defined($media);
my %attrs = ( my @similar_media = $self->media_model
select => ["tag_id"], ->similar($media->{id})->fetchall->@*;
rows => 100, my %media = map +(
); $_->{id} => $_
my $tags = $self->schema->resultset("MediaTag") ), $self->media_model
->search({media_id => $media_id}, \%attrs)->as_query; ->get(map {$_->{media_id}} @similar_media)->fetchall->@*;
my %search = ( foreach my $sm (@similar_media) {
media_id => {"!=", $media_id}, $sm = {
tag_id => {"-in", $tags}, score => $sm->{score},
); $media{$sm->{media_id}}->%*,
%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;
return $self->render(json => { return $self->render(json => {media => [@similar_media]});
media => [@media],
});
} }
1; 1;

View file

@ -3,27 +3,27 @@ use Mojo::Base "Mojolicious::Controller", -signatures;
sub media ($self) sub media ($self)
{ {
my $media = $self->schema->resultset("Media")->random->single; my $media_id = $self->media_model->random_id;
return $self->render( return $self->render(
json => {error => "Media not found."}, json => {error => "Media not found."},
status => 404, 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) sub tag ($self)
{ {
my $tag = $self->schema->resultset("Tag")->random->single; my $tag_id = $self->tags_model->random_id;
return $self->render( return $self->render(
json => {error => "Tag not found."}, json => {error => "Tag not found."},
status => 404, status => 404,
) if !defined($tag); ) if !defined($tag_id);
return $self->redirect_to( return $self->redirect_to(
$self->url_for("show_tag")->query(id => $tag->id)); $self->url_for("show_tag")->query(id => $tag_id));
} }
1; 1;

View file

@ -13,19 +13,11 @@ sub list ($self)
status => 400, status => 400,
) if $v->has_error; ) if $v->has_error;
my $paged_tags = $self->schema->resultset("TagCountView") my $paged_tags = $self->tags_model->ranked->page($page);
->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;
return $self->render(json => { return $self->render(json => {
tags => [@tags], tags => $paged_tags->fetchall,
pager => {$self->pager($paged_tags)}, pager => $paged_tags->pager,
}); });
} }
@ -41,7 +33,7 @@ sub show ($self)
status => 400, status => 400,
) if $v->has_error; ) 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; my $kind_id = $v->optional("kind_id")->param;
return $self->render( return $self->render(
@ -50,13 +42,9 @@ sub show ($self)
) if $v->has_error; ) if $v->has_error;
$search{kind_id} = $kind_id if defined($kind_id); $search{kind_id} = $kind_id if defined($kind_id);
my @tags = map +{ my @tag_ids = map {$_->{id}} $self->tags_model
id => $_->id, ->get(%search)->fetchall->@*;
name => $_->name, my @tags = $self->tags_model->ranked_for_tags(@tag_ids)->fetchall->@*;
kind_id => $_->kind_id,
display => $_->display,
count => $_->count,
}, $self->schema->resultset("TagCountView")->search(\%search)->all;
return $self->render( return $self->render(
json => {error => "Tag not found."}, json => {error => "Tag not found."},
@ -83,19 +71,7 @@ sub search ($self)
$q =~ s/[%\\_]/\\$&/g; $q =~ s/[%\\_]/\\$&/g;
$q .= '%'; $q .= '%';
my %attrs = ( my @tags = $self->tags_model->search($q)->fetchall->@*;
$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);
return $self->render(json => {tags => [@tags]}); return $self->render(json => {tags => [@tags]});
} }

View file

@ -1,5 +1,11 @@
use DBD::SQLite::Constants ":dbd_sqlite_string_mode";
{ {
dsn => "dbi:SQLite:db/pooru.db", dsn => "dbi:SQLite:db/pooru.db",
dbi_connect_args => {
# Highly recommended to keep this.
sqlite_string_mode => DBD_SQLITE_STRING_MODE_UNICODE_STRICT,
},
secrets => [ secrets => [
# Generate with "openssl rand -hex 32". # Generate with "openssl rand -hex 32".