Initial commit

This commit is contained in:
Daniel Kempkens 2023-05-18 00:13:55 +02:00
commit e03c759bb7
21 changed files with 785 additions and 0 deletions

2
.envrc Normal file
View file

@ -0,0 +1,2 @@
use flake
project elixir

8
.formatter.exs Normal file
View file

@ -0,0 +1,8 @@
[
inputs: [
"mix.exs",
"{config,lib,test}/**/*.{ex,exs}"
],
line_length: 120,
import_deps: [:plug]
]

39
.gitignore vendored Normal file
View file

@ -0,0 +1,39 @@
# The directory Mix will write compiled artifacts to.
/_build
# If you run "mix test --cover", coverage assets end up here.
/cover
# The directory Mix downloads your dependencies sources to.
/deps
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# nix
/.direnv
/.elixir_ls
# Exclude releases
/rel/*
!/rel/config.exs
!/rel/commands/
!/rel/plugins/
!/rel/vm.args.eex
!/rel/env.sh.eex
!/rel/env.bat.eex
.deliver/releases/
/result
.DS_Store
*.orig
/tmp
/tags*
/xref_graph.dot
/xref_graph.png

5
LICENSE Normal file
View file

@ -0,0 +1,5 @@
Copyright 2023 Daniel Kempkens
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

10
README.md Normal file
View file

@ -0,0 +1,10 @@
# BdfrBrowser
Maybe some else finds this useful.
It's a very crude and quickly thrown together browser for [BDFR](https://github.com/aliparlakci/bulk-downloader-for-reddit) dumps.
## Known Issues
- Directories with many posts (per month) are slow to load
- The HTML templates are very much a work in progress

6
config/config.exs Normal file
View file

@ -0,0 +1,6 @@
import Config
config :logger,
backends: [],
handle_otp_reports: false,
handle_sasl_reports: false

6
config/runtime.exs Normal file
View file

@ -0,0 +1,6 @@
import Config
config :bdfr_browser,
base_directory: System.get_env("BDFR_BROWSER_BASE_DIRECTORY", "/nonexistant"),
http_ip: to_charlist(System.get_env("BDFR_BROWSER_HTTP_IP", "127.0.0.1")),
http_port: String.to_integer(System.get_env("BDFR_BROWSER_HTTP_PORT", "4040"))

64
flake.lock Normal file
View file

@ -0,0 +1,64 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1684242266,
"narHash": "sha256-uaCQ2k1bmojHKjWQngvnnnxQJMY8zi1zq527HdWgQf8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7e0743a5aea1dc755d4b761daf75b20aa486fdad",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1682879489,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

47
flake.nix Normal file
View file

@ -0,0 +1,47 @@
{
description = "bdfr-browser development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
};
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
perSystem = { pkgs, lib, self', ... }:
let
pname = "bdfr-browser";
version = "0.0.1";
erlang = pkgs.beam.interpreters.erlangR25;
beamPackages = pkgs.beam.packagesWith erlang;
elixir = beamPackages.elixir_1_14;
inherit (pkgs.stdenv) isDarwin;
in
{
devShells.default = pkgs.mkShell {
packages = (with pkgs; [
erlang
elixir
beamPackages.elixir-ls
mix2nix
]) ++ lib.optionals isDarwin (with pkgs.darwin.apple_sdk.frameworks; [
CoreFoundation
CoreServices
]);
ERL_INCLUDE_PATH = "${erlang}/lib/erlang/usr/include";
};
packages.default = beamPackages.mixRelease {
inherit pname version;
src = ./.;
mixNixDeps = import ./mix.nix { inherit lib beamPackages; };
};
};
};
}

View file

@ -0,0 +1,21 @@
defmodule BdfrBrowser.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
{:ok, http_ip} = :inet.parse_address(Application.fetch_env!(:bdfr_browser, :http_ip))
http_port = Application.fetch_env!(:bdfr_browser, :http_port)
children = [
{Plug.Cowboy, scheme: :http, plug: BdfrBrowser.HTTP.Plug, options: [ip: http_ip, port: http_port]},
:systemd.ready()
]
opts = [strategy: :one_for_one, name: BdfrBrowser.Supervisor]
Supervisor.start_link(children, opts)
end
end

View file

@ -0,0 +1,155 @@
defmodule BdfrBrowser.HTTP.Plug do
use Plug.Router
plug :match
plug :dispatch
get "/" do
tpl_params = [
subreddits: list_folders(sort: :asc)
]
tpl_file = Application.app_dir(:bdfr_browser, "priv/templates/http/index.eex")
content = EEx.eval_file(tpl_file, tpl_params)
conn
|> put_resp_header("content-type", "text/html; charset=utf-8")
|> send_resp(200, content)
end
get "/r/:subreddit" do
tpl_params = [
subreddit: subreddit,
dates: list_folders(paths: [subreddit])
]
tpl_file = Application.app_dir(:bdfr_browser, "priv/templates/http/subreddit.eex")
content = EEx.eval_file(tpl_file, tpl_params)
conn
|> put_resp_header("content-type", "text/html; charset=utf-8")
|> send_resp(200, content)
end
get "/r/:subreddit/:date" do
tpl_params = [
subreddit: subreddit,
date: date,
posts: read_posts(paths: [subreddit, date], ext: ".json")
]
tpl_file = Application.app_dir(:bdfr_browser, "priv/templates/http/subreddit_posts.eex")
content = EEx.eval_file(tpl_file, tpl_params)
conn
|> put_resp_header("content-type", "text/html; charset=utf-8")
|> send_resp(200, content)
end
get "/r/:subreddit/:date/:post" do
tpl_params = [
subreddit: subreddit,
date: date,
post: read_post(post, paths: [subreddit, date]),
media: post_media(post, paths: [subreddit, date]),
comment_template: Application.app_dir(:bdfr_browser, "priv/templates/http/_comment.eex")
]
tpl_file = Application.app_dir(:bdfr_browser, "priv/templates/http/post.eex")
content = EEx.eval_file(tpl_file, tpl_params)
conn
|> put_resp_header("content-type", "text/html; charset=utf-8")
|> send_resp(200, content)
end
get "/media/*path" do
base_directory = Application.fetch_env!(:bdfr_browser, :base_directory)
joined_path = Path.join(path)
{:ok, media} = [base_directory, joined_path] |> Path.join() |> File.read()
conn
|> put_resp_header("content-type", mime_from_ext(joined_path))
|> send_resp(200, media)
end
match _ do
send_resp(conn, 404, "Not Found")
end
# Helper
defp list_folders(args) do
paths = Keyword.get(args, :paths, [])
extname = Keyword.get(args, :ext, "")
sort = Keyword.get(args, :sort, :desc)
base_directory = Application.fetch_env!(:bdfr_browser, :base_directory)
[base_directory | paths]
|> Path.join()
|> File.ls!()
|> Enum.filter(fn s -> not String.starts_with?(s, ".") and Path.extname(s) == extname end)
|> Enum.sort_by(&String.downcase/1, sort)
end
defp read_posts(args) do
posts = list_folders(args)
sort = Keyword.get(args, :sort, :desc)
base_directory = Application.fetch_env!(:bdfr_browser, :base_directory)
post_dir = Path.join([base_directory | Keyword.fetch!(args, :paths)])
compact_posts =
for post <- posts do
{:ok, content} = [post_dir, post] |> Path.join() |> File.read!() |> Jason.decode()
%{
title: content["title"],
author: content["author"],
num_comments: content["num_comments"],
created_utc: content["created_utc"],
filename: Path.basename(post, ".json")
}
end
Enum.sort_by(compact_posts, fn p -> p.created_utc end, sort)
end
defp read_post(post, args) do
base_directory = Application.fetch_env!(:bdfr_browser, :base_directory)
post_dir = Path.join([base_directory | Keyword.fetch!(args, :paths)])
post_file = "#{post}.json"
{:ok, content} = [post_dir, post_file] |> Path.join() |> File.read!() |> Jason.decode(keys: :atoms)
content
end
defp post_media(post, args) do
base_directory = Application.fetch_env!(:bdfr_browser, :base_directory)
post_dir = Path.join([base_directory | Keyword.fetch!(args, :paths)])
post_img = "#{post}*.{jpg,JPG,jpeg,JPEG,png,PNG,gif,GIF}"
post_vid = "#{post}*.{mp4,MP4}"
%{
images: [post_dir, post_img] |> Path.join() |> Path.wildcard() |> Enum.map(&media_path/1),
videos: [post_dir, post_vid] |> Path.join() |> Path.wildcard() |> Enum.map(&media_path/1)
}
end
defp media_path(full_path) do
base_directory = Application.fetch_env!(:bdfr_browser, :base_directory)
String.replace(full_path, "#{base_directory}/", "/media/")
end
defp mime_from_ext(path) do
normalized_path = String.downcase(path)
case Path.extname(normalized_path) do
".jpg" -> "image/jpeg"
".jpeg" -> "image/jpeg"
".png" -> "image/png"
".gif" -> "image/gif"
".mp4" -> "video/mp4"
end
end
end

29
mix.exs Normal file
View file

@ -0,0 +1,29 @@
defmodule BdfrBrowser.MixProject do
use Mix.Project
def project do
[
app: :bdfr_browser,
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger, :eex],
mod: {BdfrBrowser.Application, []}
]
end
defp deps do
[
{:plug_cowboy, "~> 2.6"},
{:jason, "~> 1.4"},
{:earmark, "~> 1.4"},
{:systemd, "~> 0.6"}
]
end
end

16
mix.lock Normal file
View file

@ -0,0 +1,16 @@
%{
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"earmark": {:hex, :earmark, "1.4.38", "ba8fda946c259c6e8f6759d3647d448e9216e2c0afed8c6ae7f8ce1f7072a497", [:mix], [{:earmark_parser, "~> 1.4.32", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "f938e30de4167e7d8f3bf588b01dc041138278dda1e5a13fb9ec89b43dd5ec7f"},
"earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
"enough": {:hex, :enough, "0.1.0", "0254710c52d324e2dadde54cb56fbb80a792c2eb285669b8379efd0752bf89f0", [:rebar3], [], "hexpm", "0460c7abda5f5e0ea592b12bc6976b8a5c4b96e42f332059cd396525374bf9a1"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"systemd": {:hex, :systemd, "0.6.2", "aaa24f1e3e6cb978c45369768b1abd766a0dbff637ed61254ca64797bcec9963", [:rebar3], [{:enough, "~> 0.1.0", [hex: :enough, repo: "hexpm", optional: false]}], "hexpm", "5062b911800c1ab05157c7bf9a9fbe23dd24c58891c87fd12d2e3ed8fc1708b8"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
}

194
mix.nix Normal file
View file

@ -0,0 +1,194 @@
{ lib, beamPackages, overrides ? (x: y: {}) }:
let
buildRebar3 = lib.makeOverridable beamPackages.buildRebar3;
buildMix = lib.makeOverridable beamPackages.buildMix;
buildErlangMk = lib.makeOverridable beamPackages.buildErlangMk;
self = packages // (overrides self packages);
packages = with beamPackages; with self; {
cowboy = buildErlangMk rec {
name = "cowboy";
version = "2.10.0";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "0sqxqjdykxc2ai9cvkc0xjwkvr80z98wzlqlrd1z3iiw32vwrz9s";
};
beamDeps = [ cowlib ranch ];
};
cowboy_telemetry = buildRebar3 rec {
name = "cowboy_telemetry";
version = "0.4.0";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "1pn90is3k9dq64wbijvzkqb6ldfqvwiqi7ymc8dx6ra5xv0vm63x";
};
beamDeps = [ cowboy telemetry ];
};
cowlib = buildRebar3 rec {
name = "cowlib";
version = "2.12.1";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "1c4dgi8canscyjgddp22mjc69znvwy44wk3r7jrl2wvs6vv76fqn";
};
beamDeps = [];
};
earmark = buildMix rec {
name = "earmark";
version = "1.4.38";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "0zzcslyv92gcp4zs3rd1vmw844s1q0fv127m7f7pszhnwh6y6f7r";
};
beamDeps = [ earmark_parser ];
};
earmark_parser = buildMix rec {
name = "earmark_parser";
version = "1.4.32";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "0md7rhw1ix4fp31bql9scvl4jpijixczm2ky7mxffwq3srvxvc5q";
};
beamDeps = [];
};
enough = buildRebar3 rec {
name = "enough";
version = "0.1.0";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "18gr9cvjar9rrmcj0crgwjb4np4adfbwcaxijajhwpjzvamwfq04";
};
beamDeps = [];
};
jason = buildMix rec {
name = "jason";
version = "1.4.0";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "0891p2yrg3ri04p302cxfww3fi16pvvw1kh4r91zg85jhl87k8vr";
};
beamDeps = [];
};
mime = buildMix rec {
name = "mime";
version = "2.0.3";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "0szzdfalafpawjrrwbrplhkgxjv8837mlxbkpbn5xlj4vgq0p8r7";
};
beamDeps = [];
};
plug = buildMix rec {
name = "plug";
version = "1.14.2";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "04wdyv6nma74bj1m49vkm2bc5mjf8zclfg957fng8g71hw0wabw4";
};
beamDeps = [ mime plug_crypto telemetry ];
};
plug_cowboy = buildMix rec {
name = "plug_cowboy";
version = "2.6.1";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "04v6xc4v741dr2y38j66fmcc4xc037dnaxzkj2vih6j53yif2dny";
};
beamDeps = [ cowboy cowboy_telemetry plug ];
};
plug_crypto = buildMix rec {
name = "plug_crypto";
version = "1.2.5";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "0hnqgzc3zas7j7wycgnkkdhaji5farkqccy2n4p1gqj5ccfrlm16";
};
beamDeps = [];
};
ranch = buildRebar3 rec {
name = "ranch";
version = "1.8.0";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "1rfz5ld54pkd2w25jadyznia2vb7aw9bclck21fizargd39wzys9";
};
beamDeps = [];
};
systemd = buildRebar3 rec {
name = "systemd";
version = "0.6.2";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "1f082zydhgif5p8pzj4ii32j9p93psgrmgy7ax8v06hch08vjqjh";
};
beamDeps = [ enough ];
};
telemetry = buildRebar3 rec {
name = "telemetry";
version = "1.2.1";
src = fetchHex {
pkg = "${name}";
version = "${version}";
sha256 = "1mgyx9zw92g6w8fp9pblm3b0bghwxwwcbslrixq23ipzisfwxnfs";
};
beamDeps = [];
};
};
in self

View file

@ -0,0 +1,23 @@
<div style="padding-left: <%= level * 5%>px;">
<div class="card">
<div class="card-body">
<blockquote class="blockquote mb-0">
<%= if comment.author == "AutoModerator" do %>
<p><small>Hidden AutoModerator comment</small></p>
<% else %>
<%= Earmark.as_html!(comment.body) %>
<% end %>
<footer class="blockquote-footer">
<%= comment.author %>,
<small><%= trunc(comment.created_utc) |> DateTime.from_unix!() |> DateTime.to_iso8601() %></small>
</footer>
</blockquote>
</div>
</div>
<br>
</div>
<%= for reply <- comment.replies do %>
<%= EEx.eval_file(comment_template, comment: reply, level: level + 1, comment_template: comment_template) %>
<% end %>

View file

@ -0,0 +1,26 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>BDFR Browser</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2>Archived Subreddits</h2>
<div class="row text-center">
<div class="d-grid gap-2 col-12 mx-auto">
<%= for subreddit <- subreddits do %>
<a class="btn btn-outline-secondary btn-lg" href="/r/<%= subreddit %>" role="button"><%= subreddit %></a>
<% end %>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,66 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>BDFR Browser</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2><a href="<%= post.url %>"><%= post.title %></a></h2>
<p>
<small><a href="https://reddit.com/user/<%= post.author %>"><%= post.author %></a></small>
-
<small><%= trunc(post.created_utc) |> DateTime.from_unix!() |> DateTime.to_iso8601() %></small>
-
<a href="https://reddit.com<%= post.permalink %>">Open reddit</a>
</p>
<%= unless Enum.empty?(media.images) do %>
<div id="carouselImages" class="carousel slide">
<div class="carousel-inner">
<%= for {img, i} <- Enum.with_index(media.images) do %>
<div class="carousel-item <%= if i == 0, do: "active" %>">
<img src="<%= img %>" class="d-block w-100">
</div>
<% end %>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselImages" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselImages" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
<br>
<% end %>
<%= unless Enum.empty?(media.videos) do %>
<div class="row">
<%= for video <- media.videos do %>
<video controls>
<source src="<%= video %>" type="video/mp4">
</video>
<% end %>
</div>
<br>
<% end %>
<%= for comment <- post.comments do %>
<div class="row">
<%= EEx.eval_file(comment_template, comment: comment, level: 0, comment_template: comment_template) %>
</div>
<br>
<% end %>
</div>
</body>
</html>

View file

@ -0,0 +1,26 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>BDFR Browser</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2><%= subreddit %></h2>
<div class="row text-center">
<div class="d-grid gap-2 col-12 mx-auto">
<%= for date <- dates do %>
<a class="btn btn-outline-secondary btn-lg" href="/r/<%= subreddit %>/<%= date %>" role="button"><%= date %></a>
<% end %>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,33 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>BDFR Browser</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2><%= subreddit %></h2>
<div class="row text-center">
<div class="d-grid gap-2 col-12 mx-auto">
<%= for post <- posts do %>
<div class="card">
<div class="card-body">
<h5 class="card-title"><a href="/r/<%= subreddit %>/<%= date %>/<%= post.filename %>"><%= post.title %></a></h5>
<h6 class="card-subtitle mb-2 text-body-secondary">
<%= post.num_comments %> comment(s) - <%= trunc(post.created_utc) |> DateTime.from_unix!() |> DateTime.to_iso8601() %>
</h6>
</div>
</div>
<% end %>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,8 @@
defmodule BdfrBrowserTest do
use ExUnit.Case
doctest BdfrBrowser
test "greets the world" do
assert BdfrBrowser.hello() == :world
end
end

1
test/test_helper.exs Normal file
View file

@ -0,0 +1 @@
ExUnit.start()