Initial import

This commit is contained in:
Lucas Gabriel Vuotto 2025-04-26 09:41:50 +00:00
commit dd492db92e
37 changed files with 1953 additions and 0 deletions

80
lib/Pooru/API/V0.pm Normal file
View file

@ -0,0 +1,80 @@
package Pooru::API::V0;
use Mojo::Base "Mojolicious", -signatures;
use DBD::SQLite::Constants ":dbd_sqlite_string_mode";
use DBIx::Migration;
use Pooru::Schema;
sub _search_opts ()
{
return {
Media => {
order_by => {-desc => "id"},
rows => 12,
},
TagCountView => {
order_by => [
{-desc => [qw(count kind_id)]},
{-asc => "name"},
],
rows => 1000,
},
TaggedMediaView_media => {
order_by => {-desc => "media_id"},
rows => 12,
},
TaggedMediaView_tags => {
order_by => [
{-desc => "tag_kind_id"},
{-asc => "tag_name"},
],
rows => 1000,
},
};
}
sub startup ($self)
{
$self->moniker("pooru-api-v0");
my $config = $self->plugin("Config");
$self->secrets($config->{secrets});
$self->helper(schema => sub {
state $schema = Pooru::Schema->connect(
$config->{dsn},
undef,
undef,
{
sqlite_string_mode =>
DBD_SQLITE_STRING_MODE_UNICODE_STRICT,
on_connect_call => "use_foreign_keys",
}
);
});
$self->helper(pager => sub ($, $rs) {
return map +($_ => ($rs->pager->$_ and int($rs->pager->$_))),
qw(first_page previous_page current_page next_page
last_page);
});
DBIx::Migration->new({
dsn => $config->{dsn},
dir => $self->app->home->child("migrations")->to_string,
})->migrate;
my $r = $self->routes;
$r->get("/meta")->to("meta#index")->name("meta");
$r->get("/media")->to("media#list")->name("list_media");
$r->get("/media/<media_id:num>")->to("media#show")->name("show_media");
$r->get("/tag")->to("tags#show")->name("show_tag");
$r->get("/tags")->to("tags#list")->name("list_tags");
$r->get("/random/media")->to("random#media")->name("random_media");
$r->get("/random/tag")->to("random#tag")->name("random_tag");
}
1;

View file

@ -0,0 +1,120 @@
package Pooru::API::V0::Controller::Media;
use Mojo::Base "Mojolicious::Controller", -signatures;
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;
return $self->render(json => {
media => [@media],
pager => {$self->pager($paged_media)},
});
}
# 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;
return $self->render(json => {
media => [@media],
pager => {$self->pager($paged_media)},
});
}
sub list ($self)
{
my $v = $self->validation;
my $page = $v->optional("page")->num(1, undef)->param // 1;
return $self->render(
json => {error => "Invalid page number."},
status => 400,
) if $v->has_error;
my @tags = split(" ", $v->optional("tags")->param // "");
return $self->render(
json => {error => "Invalid tags."},
status => 400,
) if $v->has_error;
return @tags == 0 ?
$self->_list_no_tags($page) :
$self->_list_with_tags($page, @tags);
}
sub show ($self)
{
my $v = $self->validation;
my $page = $v->optional("page")->num(1, undef)->param // 1;
return $self->render(
json => {error => "Invalid page number."},
status => 400,
) if $v->has_error;
my $media_id = $self->stash("media_id");
my $media = $self->schema->resultset("Media")
->single({id => $media_id});
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;
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)},
});
}
1;

View file

@ -0,0 +1,22 @@
package Pooru::API::V0::Controller::Meta;
use Mojo::Base "Mojolicious::Controller", -signatures;
sub index ($self)
{
return $self->render(json => {
routes => [
{ path => "/meta", verb => "GET" },
{ path => "/media", verb => "GET" },
{ path => "/media/<media_id:num>", verb => "GET" },
{ path => "/tag", verb => "GET" },
{ path => "/tags", verb => "GET" },
{ path => "/random/media", verb => "GET" },
{ path => "/random/tag", verb => "GET" },
],
});
}
1;

View file

@ -0,0 +1,35 @@
package Pooru::API::V0::Controller::Random;
use Mojo::Base "Mojolicious::Controller", -signatures;
sub _random_entry ($self, $rs)
{
return $self->schema->resultset($rs)
->search(undef, {order_by => 'random()', rows => 1})->single;
}
sub media ($self)
{
my $media = $self->_random_entry("Media");
return $self->render(
json => {error => "Media not found."},
status => 404,
) if !defined($media);
return $self->redirect_to("show_media", media_id => $media->id);
}
sub tag ($self)
{
my $tag = $self->_random_entry("Tag");
return $self->render(
json => {error => "Tag not found."},
status => 404,
) if !defined($tag);
return $self->redirect_to(
$self->url_for("show_tag")->query(id => $tag->id));
}
1;

View file

@ -0,0 +1,69 @@
package Pooru::API::V0::Controller::Tags;
use Mojo::Base "Mojolicious::Controller", -signatures;
my %search_opts = Pooru::API::V0::_search_opts->%*;
sub list ($self)
{
my $v = $self->validation;
my $page = $v->optional("page")->num(1, undef)->param // 1;
return $self->render(
json => {error => "Invalid page number."},
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;
return $self->render(json => {
tags => [@tags],
pager => {$self->pager($paged_tags)},
});
}
sub show ($self)
{
my $v = $self->validation;
my $tag_id_or_name = $v->optional("id")->num(1, undef)->param //
$v->optional("display")->param //
$v->required("name")->param;
return $self->render(
json => {error => "Invalid tag ID or name."},
status => 400,
) if $v->has_error;
my %search = ($v->topic => $tag_id_or_name);
my $kind_id = $v->optional("kind_id")->param;
return $self->render(
json => {error => "Invalid kind ID."},
status => 400,
) 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;
return $self->render(
json => {error => "Tag not found."},
status => 404,
) if @tags == 0;
return $self->render(json => {tags => [@tags]});
}
1;

20
lib/Pooru/Schema.pm Normal file
View file

@ -0,0 +1,20 @@
use utf8;
package Pooru::Schema;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Schema';
__PACKAGE__->load_namespaces;
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2025-04-26 09:36:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CKLwXldwvhv5+lWXivRpCA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -0,0 +1,96 @@
use utf8;
package Pooru::Schema::Result::Kind;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Pooru::Schema::Result::Kind
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<kind>
=cut
__PACKAGE__->table("kind");
=head1 ACCESSORS
=head2 id
data_type: 'text'
is_nullable: 0
size: 32
=head2 name
data_type: 'text'
is_nullable: 0
size: 256
=cut
__PACKAGE__->add_columns(
"id",
{ data_type => "text", is_nullable => 0, size => 32 },
"name",
{ data_type => "text", is_nullable => 0, size => 256 },
);
=head1 PRIMARY KEY
=over 4
=item * L</id>
=back
=cut
__PACKAGE__->set_primary_key("id");
=head1 UNIQUE CONSTRAINTS
=head2 C<name_unique>
=over 4
=item * L</name>
=back
=cut
__PACKAGE__->add_unique_constraint("name_unique", ["name"]);
=head1 RELATIONS
=head2 tags
Type: has_many
Related object: L<Pooru::Schema::Result::Tag>
=cut
__PACKAGE__->has_many(
"tags",
"Pooru::Schema::Result::Tag",
{ "foreign.kind_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2025-04-26 09:36:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ePfTpTOWY1H3Bky/bjA3iA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -0,0 +1,136 @@
use utf8;
package Pooru::Schema::Result::Media;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Pooru::Schema::Result::Media
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<media>
=cut
__PACKAGE__->table("media");
=head1 ACCESSORS
=head2 id
data_type: 'integer'
is_auto_increment: 1
is_nullable: 0
=head2 storage_id
data_type: 'text'
is_nullable: 0
size: 64
=head2 filename
data_type: 'text'
is_nullable: 0
size: 256
=head2 content_type
data_type: 'text'
is_nullable: 1
size: 256
=head2 upload_datetime
data_type: 'text'
default_value: current_timestamp
is_nullable: 1
size: 32
=cut
__PACKAGE__->add_columns(
"id",
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
"storage_id",
{ data_type => "text", is_nullable => 0, size => 64 },
"filename",
{ data_type => "text", is_nullable => 0, size => 256 },
"content_type",
{ data_type => "text", is_nullable => 1, size => 256 },
"upload_datetime",
{
data_type => "text",
default_value => \"current_timestamp",
is_nullable => 1,
size => 32,
},
);
=head1 PRIMARY KEY
=over 4
=item * L</id>
=back
=cut
__PACKAGE__->set_primary_key("id");
=head1 UNIQUE CONSTRAINTS
=head2 C<storage_id_unique>
=over 4
=item * L</storage_id>
=back
=cut
__PACKAGE__->add_unique_constraint("storage_id_unique", ["storage_id"]);
=head1 RELATIONS
=head2 media_tags
Type: has_many
Related object: L<Pooru::Schema::Result::MediaTag>
=cut
__PACKAGE__->has_many(
"media_tags",
"Pooru::Schema::Result::MediaTag",
{ "foreign.media_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 tags
Type: many_to_many
Composing rels: L</media_tags> -> tag
=cut
__PACKAGE__->many_to_many("tags", "media_tags", "tag");
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2025-04-26 09:36:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ODG4pqImG0JNc1YpUIVNFA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -0,0 +1,99 @@
use utf8;
package Pooru::Schema::Result::MediaTag;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Pooru::Schema::Result::MediaTag
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<media_tag>
=cut
__PACKAGE__->table("media_tag");
=head1 ACCESSORS
=head2 media_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 0
=head2 tag_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 0
=cut
__PACKAGE__->add_columns(
"media_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
"tag_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
);
=head1 PRIMARY KEY
=over 4
=item * L</media_id>
=item * L</tag_id>
=back
=cut
__PACKAGE__->set_primary_key("media_id", "tag_id");
=head1 RELATIONS
=head2 media
Type: belongs_to
Related object: L<Pooru::Schema::Result::Media>
=cut
__PACKAGE__->belongs_to(
"media",
"Pooru::Schema::Result::Media",
{ id => "media_id" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
);
=head2 tag
Type: belongs_to
Related object: L<Pooru::Schema::Result::Tag>
=cut
__PACKAGE__->belongs_to(
"tag",
"Pooru::Schema::Result::Tag",
{ id => "tag_id" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
);
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2025-04-26 09:36:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vtbD/Q3h2FEwALyG3IAaEg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -0,0 +1,157 @@
use utf8;
package Pooru::Schema::Result::Tag;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Pooru::Schema::Result::Tag
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<tag>
=cut
__PACKAGE__->table("tag");
=head1 ACCESSORS
=head2 id
data_type: 'integer'
is_auto_increment: 1
is_nullable: 0
=head2 name
data_type: 'text'
is_nullable: 0
size: 256
=head2 kind_id
data_type: 'text'
is_foreign_key: 1
is_nullable: 1
size: 32
=head2 display
data_type: 'text '
is_nullable: 1
size: 289
=cut
__PACKAGE__->add_columns(
"id",
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
"name",
{ data_type => "text", is_nullable => 0, size => 256 },
"kind_id",
{ data_type => "text", is_foreign_key => 1, is_nullable => 1, size => 32 },
"display",
{ data_type => "text ", is_nullable => 1, size => 289 },
);
=head1 PRIMARY KEY
=over 4
=item * L</id>
=back
=cut
__PACKAGE__->set_primary_key("id");
=head1 UNIQUE CONSTRAINTS
=head2 C<display_unique>
=over 4
=item * L</display>
=back
=cut
__PACKAGE__->add_unique_constraint("display_unique", ["display"]);
=head2 C<name_kind_id_unique>
=over 4
=item * L</name>
=item * L</kind_id>
=back
=cut
__PACKAGE__->add_unique_constraint("name_kind_id_unique", ["name", "kind_id"]);
=head1 RELATIONS
=head2 kind
Type: belongs_to
Related object: L<Pooru::Schema::Result::Kind>
=cut
__PACKAGE__->belongs_to(
"kind",
"Pooru::Schema::Result::Kind",
{ id => "kind_id" },
{
is_deferrable => 0,
join_type => "LEFT",
on_delete => "NO ACTION",
on_update => "CASCADE",
},
);
=head2 media_tags
Type: has_many
Related object: L<Pooru::Schema::Result::MediaTag>
=cut
__PACKAGE__->has_many(
"media_tags",
"Pooru::Schema::Result::MediaTag",
{ "foreign.tag_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 medias
Type: many_to_many
Composing rels: L</media_tags> -> media
=cut
__PACKAGE__->many_to_many("medias", "media_tags", "media");
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2025-04-26 09:36:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:XopZiRzgoNJuweuyFj9elA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -0,0 +1,76 @@
use utf8;
package Pooru::Schema::Result::TagCountView;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Pooru::Schema::Result::TagCountView
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class("DBIx::Class::ResultSource::View");
=head1 TABLE: C<tag_count_view>
=cut
__PACKAGE__->table("tag_count_view");
=head1 ACCESSORS
=head2 id
data_type: 'integer'
is_nullable: 1
=head2 name
data_type: 'text'
is_nullable: 1
size: 256
=head2 kind_id
data_type: 'text'
is_nullable: 1
size: 32
=head2 display
data_type: 'text'
is_nullable: 1
size: 289
=head2 count
data_type: (empty string)
is_nullable: 1
=cut
__PACKAGE__->add_columns(
"id",
{ data_type => "integer", is_nullable => 1 },
"name",
{ data_type => "text", is_nullable => 1, size => 256 },
"kind_id",
{ data_type => "text", is_nullable => 1, size => 32 },
"display",
{ data_type => "text", is_nullable => 1, size => 289 },
"count",
{ data_type => "", is_nullable => 1 },
);
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2025-04-26 09:36:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Z8o5//l7dE0oSJLzDguHFw
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -0,0 +1,115 @@
use utf8;
package Pooru::Schema::Result::TaggedMediaView;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Pooru::Schema::Result::TaggedMediaView
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class("DBIx::Class::ResultSource::View");
=head1 TABLE: C<tagged_media_view>
=cut
__PACKAGE__->table("tagged_media_view");
=head1 ACCESSORS
=head2 media_id
data_type: 'integer'
is_nullable: 1
=head2 media_storage_id
data_type: 'text'
is_nullable: 1
size: 64
=head2 media_filename
data_type: 'text'
is_nullable: 1
size: 256
=head2 media_content_type
data_type: 'text'
is_nullable: 1
size: 256
=head2 media_upload_datetime
data_type: 'text'
is_nullable: 1
size: 32
=head2 tag_id
data_type: 'integer'
is_nullable: 1
=head2 tag_name
data_type: 'text'
is_nullable: 1
size: 256
=head2 tag_kind_id
data_type: 'text'
is_nullable: 1
size: 32
=head2 tag_display
data_type: 'text'
is_nullable: 1
size: 289
=head2 tag_count
data_type: (empty string)
is_nullable: 1
=cut
__PACKAGE__->add_columns(
"media_id",
{ data_type => "integer", is_nullable => 1 },
"media_storage_id",
{ data_type => "text", is_nullable => 1, size => 64 },
"media_filename",
{ data_type => "text", is_nullable => 1, size => 256 },
"media_content_type",
{ data_type => "text", is_nullable => 1, size => 256 },
"media_upload_datetime",
{ data_type => "text", is_nullable => 1, size => 32 },
"tag_id",
{ data_type => "integer", is_nullable => 1 },
"tag_name",
{ data_type => "text", is_nullable => 1, size => 256 },
"tag_kind_id",
{ data_type => "text", is_nullable => 1, size => 32 },
"tag_display",
{ data_type => "text", is_nullable => 1, size => 289 },
"tag_count",
{ data_type => "", is_nullable => 1 },
);
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2025-04-26 09:36:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:yihpYS6ccFg3BurhyxXqIw
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

90
lib/Pooru/Site.pm Normal file
View file

@ -0,0 +1,90 @@
package Pooru::Site;
use Mojo::Base "Mojolicious", -signatures;
use Pooru::Storage::Static;
sub extended_pager ($self, $pager)
{
my ($start, $end);
if ($pager->{current_page} < $pager->{first_page} + 2) {
$start = $pager->{first_page};
$end = $start + 4;
} elsif ($pager->{current_page} > $pager->{last_page} - 2) {
$end = $pager->{last_page};
$start = $end - 4;
} else {
$start = $pager->{current_page} - 2;
$end = $pager->{current_page} + 2;
}
return grep {$_ >= $pager->{first_page} && $_ <= $pager->{last_page}}
($start .. $end);
}
sub link_for_tag ($self, $tag)
{
my $url = $self->url_for("list_media")
->query(tags => $tag->{display});
defined($tag->{kind_id}) and
my %class = (class => "tag-kind-$tag->{kind_id}");
return $self->link_to($tag->{display} => $url => %class);
}
sub tags_by_kind ($self, $tags)
{
my @kinds = qw(Author Character Release Tags);
my %kind_id = (
Author => "a",
Character => "c",
Release => "r",
Tags => "",
);
my @tags_by_kind;
for my $kind (@kinds) {
my @ts = grep {($_->{kind_id} // "") eq $kind_id{$kind}}
$tags->@*;
push @tags_by_kind, {kind => $kind, tags => [@ts]};
}
return @tags_by_kind;
}
sub startup ($self)
{
$self->moniker("pooru-site");
my $config = $self->plugin("Config");
$self->secrets($config->{secrets});
$self->helper(storage => sub {
state $storage = Pooru::Storage::Static->
new($config->{store}, "/s/");
});
$self->helper(api_v0_url => sub {
return Mojo::URL->new($self->config("pooru_api")->{v0});
});
$self->helper(extended_pager => \&extended_pager);
$self->helper(link_for_tag => \&link_for_tag);
$self->helper(tags_by_kind => \&tags_by_kind);
$self->ua
->connect_timeout(5)
->inactivity_timeout(5)
->max_redirects(1);
my $r = $self->routes;
$r->get("/")->to("media#list")->name("list_media");
$r->get("/media/<media_id:num>")->to("media#show")->name("show_media");
$r->get("/tags")->to("tags#list")->name("list_tags");
$r->get("/random/media")->to("random#media")->name("random_media");
$r->get("/random/tag")->to("random#tag")->name("random_tag");
}
1;

View file

@ -0,0 +1,90 @@
package Pooru::Site::Controller::Media;
use Mojo::Base "Mojolicious::Controller", -signatures;
sub list ($self)
{
my $v = $self->validation;
my $page = $v->optional("page")->num(1, undef)->param // 1;
return $self->render(
status => 400
) if $v->has_error;
my $tags = $v->optional("tags")->param;
return $self->render(
status => 400
) if $v->has_error;
$self->title($tags) if defined($tags) && $tags ne "";
$self->render_later;
my $endpoint = $self->api_v0_url->path("media")
->query(page => $page, tags => $tags);
$self->ua->get($endpoint, sub ($ua, $tx) {
return $self->render(
text => "Backend error.",
status => 500,
) if $tx->error;
my $json = $tx->res->json;
my @media = map +{
show_media_url => $self->url_for("show_media",
media_id => $_->{id}),
media_src => $self->url_for_file(
$self->storage->get($_->{id})),
}, $json->{media}->@*;
$self->stash(pager => $json->{pager});
return $self->render(
template => "gallery",
media => [@media],
);
});
}
sub show ($self)
{
my $v = $self->validation;
my $page = $v->optional("page")->num(1, undef)->param // 1;
return $self->render(
status => 400
) if $v->has_error;
my $media_id = $self->stash("media_id");
$self->render_later;
my $endpoint = $self->api_v0_url->path("media/$media_id")
->query(page => $page);
$self->ua->get($endpoint, sub ($ua, $tx) {
return $self->render(
text => "Backend error.",
status => 500,
) if $tx->error;
my $json = $tx->res->json;
my @tags = map +{
name => $_->{name},
kind_id => $_->{kind_id},
display => $_->{display},
count => $_->{count},
}, $json->{tags}->@*;
$self->stash(pager => $json->{pager});
return $self->render(
template => "media",
media => {
id => $json->{id},
filename => $json->{filename},
content_type => $json->{content_type},
upload_datetime => $json->{upload_datetime},
src => $self->url_for_file(
$self->storage->get($json->{id})),
},
tags => [@tags],
);
});
}
1;

View file

@ -0,0 +1,38 @@
package Pooru::Site::Controller::Random;
use Mojo::Base "Mojolicious::Controller", -signatures;
sub media ($self)
{
$self->render_later;
my $endpoint = $self->api_v0_url->path("random/media");
$self->ua->get($endpoint, sub ($ua, $tx) {
return $self->render(
text => "Backend error.",
status => 500,
) if $tx->error;
return $self->redirect_to("show_media",
media_id => $tx->res->json->{id});
});
}
sub tag ($self)
{
$self->render_later;
my $endpoint = $self->api_v0_url->path("random/tag");
$self->ua->get($endpoint, sub ($ua, $tx) {
return $self->render(
text => "Backend error.",
status => 500,
) if $tx->error;
my @tags = $tx->res->json->{tags}->@*;
my $url = $self->url_for("list_media")
->query(tags => $tags[int(rand(@tags))]->{display});
return $self->redirect_to($url);
});
}
1;

View file

@ -0,0 +1,38 @@
package Pooru::Site::Controller::Tags;
use Mojo::Base "Mojolicious::Controller", -signatures;
sub list ($self)
{
my $v = $self->validation;
my $page = $v->optional("page")->num(1, undef)->param // 1;
return $self->render(
status => 400
) if $v->has_error;
$self->render_later;
my $endpoint = $self->api_v0_url->path("tags")->query(page => $page);
$self->ua->get($endpoint, sub ($ua, $tx) {
return $self->render(
text => "Backend error.",
status => 500,
) if $tx->error;
my $json = $tx->res->json;
my @tags = map +{
name => $_->{name},
kind_id => $_->{kind_id},
display => $_->{display},
count => $_->{count},
}, $json->{tags}->@*;
$self->stash(pager => $json->{pager});
return $self->render(
template => "tags",
tags => [@tags],
);
});
}
1;

17
lib/Pooru/Storage.pm Normal file
View file

@ -0,0 +1,17 @@
package Pooru::Storage;
use v5.40;
use Carp;
sub new ($class) {
return bless {}, $class;
}
sub get ($self, $id)
{
croak "not implemented";
}
sub put ($self, $name)
{
croak "not implemented";
}

View file

@ -0,0 +1,40 @@
package Pooru::Storage::Static;
use v5.40;
use parent "Pooru::Storage";
use Storable qw(lock_store lock_retrieve);
sub new ($class, $file, $prefix = "")
{
my $self = $class->SUPER::new;
$self->{_file} = $file;
$self->{_prefix} = $prefix;
if (-f $file) {
$self->{_store} = lock_retrieve($file);
} else {
$self->{_store} = {
_last_id => 0,
_entries => {},
};
}
return $self;
}
sub get ($self, $id)
{
my $entry = $self->{_store}->{_entries}->{$id};
return (defined($entry) and "$self->{_prefix}$entry->{name}");
}
sub put ($self, $name)
{
my $id = ++$self->{_store}->{_last_id};
$self->{_store}->{_entries}->{$id} = {
name => $name,
};
return (lock_store($self->{_store}, $self->{_file}) and $id);
}