Initial import
This commit is contained in:
commit
1ff831f80d
15
bin/app.psgi
Executable file
15
bin/app.psgi
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use FindBin;
|
||||||
|
use lib "$FindBin::Bin/../lib";
|
||||||
|
|
||||||
|
use PoorBooru;
|
||||||
|
use PoorBooru::API::V0;
|
||||||
|
use Plack::Builder;
|
||||||
|
|
||||||
|
builder {
|
||||||
|
mount "/" => PoorBooru->to_app;
|
||||||
|
mount "/api/v0" => PoorBooru::API::V0->to_app;
|
||||||
|
};
|
62
bin/create-dbix-class-schemas
Executable file
62
bin/create-dbix-class-schemas
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
err()
|
||||||
|
{
|
||||||
|
printf "%s: %s\n" "${0##*/}" "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
warn()
|
||||||
|
{
|
||||||
|
printf "%s: %s\n" "${0##*/}" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
check_required_programs()
|
||||||
|
{
|
||||||
|
_rc=0
|
||||||
|
for _prog; do
|
||||||
|
if ! command -v "$_prog" >/dev/null; then
|
||||||
|
_rc=1
|
||||||
|
warn "$_prog: not found"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return $_rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
check_required_programs sqlite3 dbicdump || exit 1
|
||||||
|
|
||||||
|
sqlite3 "db/PoorBooru.db" <<'_SQL'
|
||||||
|
PRAGMA foreig_keys = ON;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS media(
|
||||||
|
media_id INTEGER PRIMARY KEY,
|
||||||
|
content BLOB (10485760) NOT NULL,
|
||||||
|
filename TEXT (255) NOT NULL,
|
||||||
|
content_type TEXT (255)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tags(
|
||||||
|
tag_id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT (255) NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS media_tags(
|
||||||
|
media_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (media_id) REFERENCES media (media_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (tag_id) REFERENCES tags (tag_id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY(media_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE VIEW IF NOT EXISTS tags_count_view AS SELECT
|
||||||
|
tags.tag_id AS tag_id,
|
||||||
|
tags.name AS name,
|
||||||
|
COUNT(media_tags.media_id) AS count
|
||||||
|
FROM tags
|
||||||
|
INNER JOIN media_tags USING (tag_id)
|
||||||
|
GROUP BY tag_id;
|
||||||
|
|
||||||
|
_SQL
|
||||||
|
|
||||||
|
dbicdump -o dump_directory=./lib PoorBooru::Schema dbi:SQLite:db/PoorBooru.db
|
68
config.yml
Normal file
68
config.yml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# This is the main configuration file of your Dancer2 app
|
||||||
|
# env-related settings should go to environments/$env.yml
|
||||||
|
# all the settings in this file will be loaded at Dancer's startup.
|
||||||
|
|
||||||
|
# === Basic configuration ===
|
||||||
|
|
||||||
|
# Your application's name
|
||||||
|
appname: "PoorBooru"
|
||||||
|
|
||||||
|
# The default layout to use for your application (located in
|
||||||
|
# views/layouts/main.tt)
|
||||||
|
layout: "main"
|
||||||
|
|
||||||
|
# when the charset is set to UTF-8 Dancer2 will handle for you
|
||||||
|
# all the magic of encoding and decoding. You should not care
|
||||||
|
# about unicode within your app when this setting is set (recommended).
|
||||||
|
charset: "UTF-8"
|
||||||
|
|
||||||
|
# === Engines ===
|
||||||
|
#
|
||||||
|
# NOTE: All the engine configurations need to be under a single "engines:"
|
||||||
|
# key. If you uncomment engine configurations below, make sure to delete
|
||||||
|
# all "engines:" lines except the first. Otherwise, only the last
|
||||||
|
# "engines:" block will take effect.
|
||||||
|
|
||||||
|
# template engine
|
||||||
|
# simple: default and very basic template engine
|
||||||
|
# template_toolkit: TT
|
||||||
|
|
||||||
|
template: "tiny"
|
||||||
|
|
||||||
|
# template: "template_toolkit"
|
||||||
|
# engines:
|
||||||
|
# template:
|
||||||
|
# template_toolkit:
|
||||||
|
# # Note: start_tag and end_tag are regexes
|
||||||
|
# start_tag: '<%'
|
||||||
|
# end_tag: '%>'
|
||||||
|
|
||||||
|
# session engine
|
||||||
|
#
|
||||||
|
# Simple: in-memory session store - Dancer2::Session::Simple
|
||||||
|
# YAML: session stored in YAML files - Dancer2::Session::YAML
|
||||||
|
#
|
||||||
|
# Check out metacpan for other session storage options:
|
||||||
|
# https://metacpan.org/search?q=Dancer2%3A%3ASession&search_type=modules
|
||||||
|
#
|
||||||
|
# Default value for 'cookie_name' is 'dancer.session'. If you run multiple
|
||||||
|
# Dancer apps on the same host then you will need to make sure 'cookie_name'
|
||||||
|
# is different for each app.
|
||||||
|
#
|
||||||
|
#engines:
|
||||||
|
# session:
|
||||||
|
# Simple:
|
||||||
|
# cookie_name: testapp.session
|
||||||
|
#
|
||||||
|
#engines:
|
||||||
|
# session:
|
||||||
|
# YAML:
|
||||||
|
# cookie_name: eshop.session
|
||||||
|
# is_secure: 1
|
||||||
|
# is_http_only: 1
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
DBIC:
|
||||||
|
default:
|
||||||
|
dsn: "dbi:SQLite:dbname=db/PoorBooru.db"
|
||||||
|
schema_class: PoorBooru::Schema
|
34
cpanfile
Normal file
34
cpanfile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
requires "Dancer2" => "0.400000";
|
||||||
|
|
||||||
|
recommends "YAML" => "0";
|
||||||
|
recommends "URL::Encode::XS" => "0";
|
||||||
|
recommends "CGI::Deurl::XS" => "0";
|
||||||
|
recommends "CBOR::XS" => "0";
|
||||||
|
recommends "YAML::XS" => "0";
|
||||||
|
recommends "Class::XSAccessor" => "0";
|
||||||
|
recommends "Crypt::URandom" => "0";
|
||||||
|
recommends "HTTP::XSCookies" => "0";
|
||||||
|
recommends "HTTP::XSHeaders" => "0";
|
||||||
|
recommends "Math::Random::ISAAC::XS" => "0";
|
||||||
|
recommends "MooX::TypeTiny" => "0";
|
||||||
|
recommends "Type::Tiny::XS" => "0";
|
||||||
|
|
||||||
|
feature 'accelerate', 'Accelerate Dancer2 app performance with XS modules' => sub {
|
||||||
|
requires "URL::Encode::XS" => "0";
|
||||||
|
requires "CGI::Deurl::XS" => "0";
|
||||||
|
requires "YAML::XS" => "0";
|
||||||
|
requires "Class::XSAccessor" => "0";
|
||||||
|
requires "Cpanel::JSON::XS" => "0";
|
||||||
|
requires "Crypt::URandom" => "0";
|
||||||
|
requires "HTTP::XSCookies" => "0";
|
||||||
|
requires "HTTP::XSHeaders" => "0";
|
||||||
|
requires "Math::Random::ISAAC::XS" => "0";
|
||||||
|
requires "MooX::TypeTiny" => "0";
|
||||||
|
requires "Type::Tiny::XS" => "0";
|
||||||
|
};
|
||||||
|
|
||||||
|
on "test" => sub {
|
||||||
|
requires "Test::More" => "0";
|
||||||
|
requires "HTTP::Request::Common" => "0";
|
||||||
|
};
|
||||||
|
|
22
environments/development.yml
Normal file
22
environments/development.yml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# configuration file for development environment
|
||||||
|
|
||||||
|
# the logger engine to use
|
||||||
|
# console: log messages to STDOUT (your console where you started the
|
||||||
|
# application server)
|
||||||
|
# file: log message to a file in log/
|
||||||
|
logger: "console"
|
||||||
|
|
||||||
|
# the log level for this environment
|
||||||
|
# core is the lowest, it shows Dancer2's core log messages as well as yours
|
||||||
|
# (debug, info, warning and error)
|
||||||
|
log: "core"
|
||||||
|
|
||||||
|
# should Dancer2 show a stacktrace when an 5xx error is caught?
|
||||||
|
# if set to yes, public/500.html will be ignored and either
|
||||||
|
# views/500.tt, 'error_template' template, or a default error template will be used.
|
||||||
|
show_errors: 1
|
||||||
|
|
||||||
|
# print the banner
|
||||||
|
startup_info: 1
|
||||||
|
|
||||||
|
poorbooru_api: "http://localhost:8080/api/v0"
|
13
environments/production.yml
Normal file
13
environments/production.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# configuration file for production environment
|
||||||
|
|
||||||
|
# only log warning and error messsages
|
||||||
|
log: "warning"
|
||||||
|
|
||||||
|
# log message to a file in logs/
|
||||||
|
logger: "file"
|
||||||
|
|
||||||
|
# hide errors
|
||||||
|
show_errors: 0
|
||||||
|
|
||||||
|
# disable server tokens in production environments
|
||||||
|
no_server_tokens: 1
|
37
lib/PoorBooru.pm
Normal file
37
lib/PoorBooru.pm
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package PoorBooru;
|
||||||
|
use Dancer2;
|
||||||
|
use HTTP::Tiny;
|
||||||
|
|
||||||
|
our $VERSION = v0.1;
|
||||||
|
|
||||||
|
my $POORBOORU_API = setting("poorbooru_api");
|
||||||
|
|
||||||
|
hook before_template_render => sub {
|
||||||
|
my $tokens = shift;
|
||||||
|
|
||||||
|
$tokens->{uris}->{root} = uri_for("/");
|
||||||
|
$tokens->{uris}->{login} = uri_for("/login");
|
||||||
|
$tokens->{uris}->{logout} = uri_for("/logout");
|
||||||
|
$tokens->{uris}->{random} = uri_for("/random");
|
||||||
|
$tokens->{uris}->{tags} = uri_for("/tags");
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/" => sub {
|
||||||
|
template "index" => {
|
||||||
|
"title" => "main",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/tags" => sub {
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/tag/:tag_id" => sub {
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/random" => sub {
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/image/:image_id" => sub {
|
||||||
|
};
|
||||||
|
|
||||||
|
true;
|
113
lib/PoorBooru/API/V0.pm
Normal file
113
lib/PoorBooru/API/V0.pm
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package PoorBooru::API::V0;
|
||||||
|
use Dancer2;
|
||||||
|
use Dancer2::Plugin::DBIC;
|
||||||
|
|
||||||
|
our $VERSION = v0;
|
||||||
|
|
||||||
|
set serializer => "JSON";
|
||||||
|
set database => "db/booru.db";
|
||||||
|
|
||||||
|
my $DEFAULT_CONTENT_TYPE = "application/json";
|
||||||
|
my @ROUTES = (
|
||||||
|
{ path => "/meta", verb => "GET" },
|
||||||
|
{ path => "/tags", verb => "GET" },
|
||||||
|
{ path => "/tags/new", verb => "POST" },
|
||||||
|
{ path => "/tag/:tag_id_or_name", verb => "GET" },
|
||||||
|
{ path => "/random", verb => "GET" },
|
||||||
|
{ path => "/media/:media_id", verb => "GET" },
|
||||||
|
{ path => "/download/:media_id", verb => "GET" },
|
||||||
|
);
|
||||||
|
|
||||||
|
get "/meta" => sub {
|
||||||
|
return \@ROUTES;
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/tags" => sub {
|
||||||
|
my @tags = schema("default")->resultset("TagsCountView")->all;
|
||||||
|
|
||||||
|
return [
|
||||||
|
map +( {
|
||||||
|
id => $_->tag_id,
|
||||||
|
name => $_->name,
|
||||||
|
count => $_->count,
|
||||||
|
} ), @tags,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
post "/tags/new" => sub {
|
||||||
|
my $tag;
|
||||||
|
|
||||||
|
eval {
|
||||||
|
$tag = schema("default")->resultset("Tag")
|
||||||
|
->create({ name => body_parameters->get("name") });
|
||||||
|
} or send_error("Tag exists", 409);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id => $tag->tag_id,
|
||||||
|
name => $tag->name,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/tag/:tag_id_or_name" => sub {
|
||||||
|
my ($tag, $tag_rset, @media);
|
||||||
|
my $tag_id_or_name = route_parameters->get("tag_id_or_name");
|
||||||
|
|
||||||
|
$tag_rset = schema("default")->resultset("Tag");
|
||||||
|
$tag = $tag_rset->single({ tag_id => $tag_id_or_name }) //
|
||||||
|
$tag_rset->single({ name => $tag_id_or_name });
|
||||||
|
|
||||||
|
send_error("Tag not found", 404) if !defined($tag);
|
||||||
|
|
||||||
|
@media = map { $_->media_id } schema("default")->resultset("MediaTag")
|
||||||
|
->search({ tag_id => $tag->tag_id })->all;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id => $tag->tag_id,
|
||||||
|
name => $tag->name,
|
||||||
|
media => [
|
||||||
|
map +( {
|
||||||
|
id => $_,
|
||||||
|
uri => uri_for("/media/$_"),
|
||||||
|
donwload_uri => uri_for("/download/$_"),
|
||||||
|
} ), @media,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/random" => sub {
|
||||||
|
my $media = schema("default")->resultset("Media")
|
||||||
|
->search({}, { order_by => \"random()", limit => 1 })->single;
|
||||||
|
|
||||||
|
send_error("Media not found", 404) if !defined($media);
|
||||||
|
|
||||||
|
forward "/media/" . $media->media_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/media/:media_id" => sub {
|
||||||
|
my $media = schema("default")->resultset("Media")
|
||||||
|
->single({ media_id => route_parameters->get("media_id") });
|
||||||
|
|
||||||
|
send_error("Media not found", 404) if !defined($media);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id => $media->media_id,
|
||||||
|
filename => $media->filename,
|
||||||
|
size => length($media->content),
|
||||||
|
download_uri => uri_for("/download/" . $media->media_id),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
get "/download/:media_id" => sub {
|
||||||
|
my $media = schema("default")->resultset("Media")
|
||||||
|
->single({ media_id => route_parameters->get("media_id") });
|
||||||
|
|
||||||
|
send_error("Media not found", 404) if !defined($media);
|
||||||
|
|
||||||
|
send_file(
|
||||||
|
\$media->content,
|
||||||
|
content_type => $media->content_type // $DEFAULT_CONTENT_TYPE,
|
||||||
|
filename => $media->filename,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
true;
|
20
lib/PoorBooru/Schema.pm
Normal file
20
lib/PoorBooru/Schema.pm
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use utf8;
|
||||||
|
package PoorBooru::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.07049 @ 2023-02-18 09:09:31
|
||||||
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:0PabNwBpp04P3y4a8O5MtA
|
||||||
|
|
||||||
|
|
||||||
|
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||||
|
1;
|
108
lib/PoorBooru/Schema/Result/Media.pm
Normal file
108
lib/PoorBooru/Schema/Result/Media.pm
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
use utf8;
|
||||||
|
package PoorBooru::Schema::Result::Media;
|
||||||
|
|
||||||
|
# Created by DBIx::Class::Schema::Loader
|
||||||
|
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||||
|
|
||||||
|
=head1 NAME
|
||||||
|
|
||||||
|
PoorBooru::Schema::Result::Media
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use base 'DBIx::Class::Core';
|
||||||
|
|
||||||
|
=head1 TABLE: C<media>
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->table("media");
|
||||||
|
|
||||||
|
=head1 ACCESSORS
|
||||||
|
|
||||||
|
=head2 media_id
|
||||||
|
|
||||||
|
data_type: 'integer'
|
||||||
|
is_auto_increment: 1
|
||||||
|
is_nullable: 0
|
||||||
|
|
||||||
|
=head2 content
|
||||||
|
|
||||||
|
data_type: 'blob'
|
||||||
|
is_nullable: 0
|
||||||
|
size: 10485760
|
||||||
|
|
||||||
|
=head2 filename
|
||||||
|
|
||||||
|
data_type: 'text'
|
||||||
|
is_nullable: 0
|
||||||
|
size: 255
|
||||||
|
|
||||||
|
=head2 content_type
|
||||||
|
|
||||||
|
data_type: 'text'
|
||||||
|
is_nullable: 1
|
||||||
|
size: 255
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->add_columns(
|
||||||
|
"media_id",
|
||||||
|
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
|
||||||
|
"content",
|
||||||
|
{ data_type => "blob", is_nullable => 0, size => 10485760 },
|
||||||
|
"filename",
|
||||||
|
{ data_type => "text", is_nullable => 0, size => 255 },
|
||||||
|
"content_type",
|
||||||
|
{ data_type => "text", is_nullable => 1, size => 255 },
|
||||||
|
);
|
||||||
|
|
||||||
|
=head1 PRIMARY KEY
|
||||||
|
|
||||||
|
=over 4
|
||||||
|
|
||||||
|
=item * L</media_id>
|
||||||
|
|
||||||
|
=back
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->set_primary_key("media_id");
|
||||||
|
|
||||||
|
=head1 RELATIONS
|
||||||
|
|
||||||
|
=head2 media_tags
|
||||||
|
|
||||||
|
Type: has_many
|
||||||
|
|
||||||
|
Related object: L<PoorBooru::Schema::Result::MediaTag>
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->has_many(
|
||||||
|
"media_tags",
|
||||||
|
"PoorBooru::Schema::Result::MediaTag",
|
||||||
|
{ "foreign.media_id" => "self.media_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.07049 @ 2023-02-18 09:09:31
|
||||||
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:X0A6CSw6yWOYvxhMNRoS0g
|
||||||
|
|
||||||
|
|
||||||
|
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||||
|
1;
|
99
lib/PoorBooru/Schema/Result/MediaTag.pm
Normal file
99
lib/PoorBooru/Schema/Result/MediaTag.pm
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
use utf8;
|
||||||
|
package PoorBooru::Schema::Result::MediaTag;
|
||||||
|
|
||||||
|
# Created by DBIx::Class::Schema::Loader
|
||||||
|
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||||
|
|
||||||
|
=head1 NAME
|
||||||
|
|
||||||
|
PoorBooru::Schema::Result::MediaTag
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use base 'DBIx::Class::Core';
|
||||||
|
|
||||||
|
=head1 TABLE: C<media_tags>
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->table("media_tags");
|
||||||
|
|
||||||
|
=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<PoorBooru::Schema::Result::Media>
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->belongs_to(
|
||||||
|
"media",
|
||||||
|
"PoorBooru::Schema::Result::Media",
|
||||||
|
{ media_id => "media_id" },
|
||||||
|
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
|
||||||
|
);
|
||||||
|
|
||||||
|
=head2 tag
|
||||||
|
|
||||||
|
Type: belongs_to
|
||||||
|
|
||||||
|
Related object: L<PoorBooru::Schema::Result::Tag>
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->belongs_to(
|
||||||
|
"tag",
|
||||||
|
"PoorBooru::Schema::Result::Tag",
|
||||||
|
{ tag_id => "tag_id" },
|
||||||
|
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-02-18 09:09:31
|
||||||
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CaXiI8+eaCoDbot368Hjyw
|
||||||
|
|
||||||
|
|
||||||
|
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||||
|
1;
|
106
lib/PoorBooru/Schema/Result/Tag.pm
Normal file
106
lib/PoorBooru/Schema/Result/Tag.pm
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
use utf8;
|
||||||
|
package PoorBooru::Schema::Result::Tag;
|
||||||
|
|
||||||
|
# Created by DBIx::Class::Schema::Loader
|
||||||
|
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||||
|
|
||||||
|
=head1 NAME
|
||||||
|
|
||||||
|
PoorBooru::Schema::Result::Tag
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use base 'DBIx::Class::Core';
|
||||||
|
|
||||||
|
=head1 TABLE: C<tags>
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->table("tags");
|
||||||
|
|
||||||
|
=head1 ACCESSORS
|
||||||
|
|
||||||
|
=head2 tag_id
|
||||||
|
|
||||||
|
data_type: 'integer'
|
||||||
|
is_auto_increment: 1
|
||||||
|
is_nullable: 0
|
||||||
|
|
||||||
|
=head2 name
|
||||||
|
|
||||||
|
data_type: 'text'
|
||||||
|
is_nullable: 0
|
||||||
|
size: 255
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->add_columns(
|
||||||
|
"tag_id",
|
||||||
|
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
|
||||||
|
"name",
|
||||||
|
{ data_type => "text", is_nullable => 0, size => 255 },
|
||||||
|
);
|
||||||
|
|
||||||
|
=head1 PRIMARY KEY
|
||||||
|
|
||||||
|
=over 4
|
||||||
|
|
||||||
|
=item * L</tag_id>
|
||||||
|
|
||||||
|
=back
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->set_primary_key("tag_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 media_tags
|
||||||
|
|
||||||
|
Type: has_many
|
||||||
|
|
||||||
|
Related object: L<PoorBooru::Schema::Result::MediaTag>
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->has_many(
|
||||||
|
"media_tags",
|
||||||
|
"PoorBooru::Schema::Result::MediaTag",
|
||||||
|
{ "foreign.tag_id" => "self.tag_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.07049 @ 2023-02-18 09:09:31
|
||||||
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tlbIFVg6S6LAWWiR0ioHFw
|
||||||
|
|
||||||
|
|
||||||
|
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||||
|
1;
|
60
lib/PoorBooru/Schema/Result/TagsCountView.pm
Normal file
60
lib/PoorBooru/Schema/Result/TagsCountView.pm
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use utf8;
|
||||||
|
package PoorBooru::Schema::Result::TagsCountView;
|
||||||
|
|
||||||
|
# Created by DBIx::Class::Schema::Loader
|
||||||
|
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||||
|
|
||||||
|
=head1 NAME
|
||||||
|
|
||||||
|
PoorBooru::Schema::Result::TagsCountView
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use base 'DBIx::Class::Core';
|
||||||
|
__PACKAGE__->table_class("DBIx::Class::ResultSource::View");
|
||||||
|
|
||||||
|
=head1 TABLE: C<tags_count_view>
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->table("tags_count_view");
|
||||||
|
|
||||||
|
=head1 ACCESSORS
|
||||||
|
|
||||||
|
=head2 tag_id
|
||||||
|
|
||||||
|
data_type: 'integer'
|
||||||
|
is_nullable: 1
|
||||||
|
|
||||||
|
=head2 name
|
||||||
|
|
||||||
|
data_type: 'text'
|
||||||
|
is_nullable: 1
|
||||||
|
size: 255
|
||||||
|
|
||||||
|
=head2 count
|
||||||
|
|
||||||
|
data_type: (empty string)
|
||||||
|
is_nullable: 1
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
__PACKAGE__->add_columns(
|
||||||
|
"tag_id",
|
||||||
|
{ data_type => "integer", is_nullable => 1 },
|
||||||
|
"name",
|
||||||
|
{ data_type => "text", is_nullable => 1, size => 255 },
|
||||||
|
"count",
|
||||||
|
{ data_type => "", is_nullable => 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-02-18 09:09:31
|
||||||
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9NUzqweyXVu/G84fxb3eew
|
||||||
|
|
||||||
|
|
||||||
|
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||||
|
1;
|
150
public/css/style.css
Normal file
150
public/css/style.css
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* PoorBooru - Poorman's booru
|
||||||
|
*
|
||||||
|
* Written in 2023 by Lucas
|
||||||
|
*
|
||||||
|
* To the extent possible under law, the author(s) have dedicated all
|
||||||
|
* copyright and related and neighboring rights to this software to the
|
||||||
|
* public domain worldwide. This software is distributed without any
|
||||||
|
* warranty.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the CC0 Public Domain Dedication
|
||||||
|
* along with this software. If not, see
|
||||||
|
* <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* Some parts copied from https://piccalil.li/blog/a-modern-css-reset/
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-color: #0c0700;
|
||||||
|
--fg-color: #fff8f0;
|
||||||
|
--accent-color-0: #ec57bc;
|
||||||
|
--accent-color-1: #f7be00;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
h1,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--fg-color);
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
p {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer > p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent-color-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link,
|
||||||
|
a:visited {
|
||||||
|
color: var(--accent-color-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:active {
|
||||||
|
color: var(--accent-color-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 2.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 60rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-bottom {
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-top {
|
||||||
|
border-top: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-accent {
|
||||||
|
border-color: var(--accent-color-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-color {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fg-color {
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fg-accent {
|
||||||
|
color: var(--accent-color-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:link,
|
||||||
|
.nav-link:visited {
|
||||||
|
color: var(--accent-color-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover,
|
||||||
|
.nav-link:active {
|
||||||
|
color: var(--accent-color-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-gap {
|
||||||
|
gap: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-c-horizontal {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-c-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-c-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-i-fullsize {
|
||||||
|
flex: auto;
|
||||||
|
}
|
5
t/001_base.t
Normal file
5
t/001_base.t
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Test::More tests => 1;
|
||||||
|
use_ok 'PoorBooru';
|
16
t/002_index_route.t
Normal file
16
t/002_index_route.t
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use PoorBooru;
|
||||||
|
use Test::More tests => 2;
|
||||||
|
use Plack::Test;
|
||||||
|
use HTTP::Request::Common;
|
||||||
|
use Ref::Util qw<is_coderef>;
|
||||||
|
|
||||||
|
my $app = PoorBooru->to_app;
|
||||||
|
ok( is_coderef($app), 'Got app' );
|
||||||
|
|
||||||
|
my $test = Plack::Test->create($app);
|
||||||
|
my $res = $test->request( GET '/' );
|
||||||
|
|
||||||
|
ok( $res->is_success, '[GET /] successful' );
|
1
views/index.tt
Normal file
1
views/index.tt
Normal file
@ -0,0 +1 @@
|
|||||||
|
<h1>No content</h1>
|
27
views/layouts/main.tt
Normal file
27
views/layouts/main.tt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="[% settings.charset %]">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||||
|
<title>[% settings.appname %] - [% title %]</title>
|
||||||
|
<link rel="stylesheet" href="[% request.uri_base %]/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-default fg-default flex-c-vertical">
|
||||||
|
<header class="border-bottom border-accent">
|
||||||
|
<nav class="viewport flex-c-horizontal nav-link-gap">
|
||||||
|
<a class="nav-link" href="[% uris.root %]">[% settings.appname %]</a>
|
||||||
|
<a class="nav-link" href="[% uris.random %]">random</a>
|
||||||
|
<a class="nav-link" href="[% uris.tags %]">tags</a>
|
||||||
|
<span class="flex-i-fullsize"><!-- spacer --></span>
|
||||||
|
<a class="nav-link" href="[% uris.login %]">login</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<!--<main class="viewport flex-i-fullsize flex-c-vertical flex-c-center">-->
|
||||||
|
<main class="viewport flex-i-fullsize">
|
||||||
|
[% content -%]
|
||||||
|
</main>
|
||||||
|
<footer class="text-center">
|
||||||
|
<p class="viewport">Powered by <a href="https://www.openbsd.org">OpenBSD</a> and <a href="https://perldancer.org/">Dancer2</a></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user