Initial import
This commit is contained in:
commit
dd492db92e
37 changed files with 1953 additions and 0 deletions
80
lib/Pooru/API/V0.pm
Normal file
80
lib/Pooru/API/V0.pm
Normal 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;
|
120
lib/Pooru/API/V0/Controller/Media.pm
Normal file
120
lib/Pooru/API/V0/Controller/Media.pm
Normal 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;
|
22
lib/Pooru/API/V0/Controller/Meta.pm
Normal file
22
lib/Pooru/API/V0/Controller/Meta.pm
Normal 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;
|
35
lib/Pooru/API/V0/Controller/Random.pm
Normal file
35
lib/Pooru/API/V0/Controller/Random.pm
Normal 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;
|
69
lib/Pooru/API/V0/Controller/Tags.pm
Normal file
69
lib/Pooru/API/V0/Controller/Tags.pm
Normal 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
20
lib/Pooru/Schema.pm
Normal 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;
|
96
lib/Pooru/Schema/Result/Kind.pm
Normal file
96
lib/Pooru/Schema/Result/Kind.pm
Normal 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;
|
136
lib/Pooru/Schema/Result/Media.pm
Normal file
136
lib/Pooru/Schema/Result/Media.pm
Normal 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;
|
99
lib/Pooru/Schema/Result/MediaTag.pm
Normal file
99
lib/Pooru/Schema/Result/MediaTag.pm
Normal 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;
|
157
lib/Pooru/Schema/Result/Tag.pm
Normal file
157
lib/Pooru/Schema/Result/Tag.pm
Normal 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;
|
76
lib/Pooru/Schema/Result/TagCountView.pm
Normal file
76
lib/Pooru/Schema/Result/TagCountView.pm
Normal 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;
|
115
lib/Pooru/Schema/Result/TaggedMediaView.pm
Normal file
115
lib/Pooru/Schema/Result/TaggedMediaView.pm
Normal 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
90
lib/Pooru/Site.pm
Normal 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;
|
90
lib/Pooru/Site/Controller/Media.pm
Normal file
90
lib/Pooru/Site/Controller/Media.pm
Normal 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;
|
38
lib/Pooru/Site/Controller/Random.pm
Normal file
38
lib/Pooru/Site/Controller/Random.pm
Normal 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;
|
38
lib/Pooru/Site/Controller/Tags.pm
Normal file
38
lib/Pooru/Site/Controller/Tags.pm
Normal 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
17
lib/Pooru/Storage.pm
Normal 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";
|
||||
}
|
40
lib/Pooru/Storage/Static.pm
Normal file
40
lib/Pooru/Storage/Static.pm
Normal 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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue