Remove views and replace redis with sequel

master
Guillaume Dott 2019-06-28 12:38:52 +02:00
parent 46179cd27b
commit eef60a8420
25 changed files with 122 additions and 778 deletions

View File

@ -0,0 +1,17 @@
Sequel.migration do
change do
create_table(:genres) do
primary_key :id
String :name, null: false
end
create_table(:movies) do
primary_key :id
String :data, text: true, null: false
FalseClass :viewed, null: false, default: false
FalseClass :downloaded, null: false, default: false
end
create_join_table(movie_id: :movies, genre_id: :genres)
end
end

View File

@ -1,15 +1,11 @@
require 'librarix/version' require 'librarix/version'
require 'librarix/redis'
require 'librarix/redis/movie'
require 'sequel'
require 'sqlite3'
require 'json'
Sequel::Model.plugin :json_serializer
Sequel::Model.db = Sequel.sqlite('librarix.sqlite')
require 'librarix/models'
require 'librarix/application' require 'librarix/application'
module Librarix
def self.redis
@redis ||= Librarix::Redis.new(::Redis.new)
end
def self.redis=(connection, namespace: 'librarix')
@redis = Librarix::Redis.new(connection, namespace: namespace)
end
end

View File

@ -1,31 +1,17 @@
require 'librarix/filter'
require 'librarix/menu'
require 'librarix/the_movie_db'
require 'librarix/helpers'
require 'sinatra/base' require 'sinatra/base'
require 'sinatra/content_for'
require 'sinatra/json' require 'sinatra/json'
require 'sinatra/namespace' require 'sinatra/namespace'
require 'yaml' require 'yaml'
require 'slim'
require 'themoviedb' require 'themoviedb'
module Librarix module Librarix
class Application < Sinatra::Application class Application < Sinatra::Application
def initialize(app = nil) namespace '/api/v1' do
super not_found do
{error: :not_found}.to_json
Librarix::Menu.menu.add 'Home', '/'
Librarix::Menu.menu.add 'Add a movie', '/search'
end end
helpers Librarix::TheMovieDB
helpers Librarix::Menu::Helper
helpers Librarix::Helpers
namespace '/api/v1' do
helpers do helpers do
def base_url def base_url
@base_url ||= "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}" @base_url ||= "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}"
@ -44,11 +30,18 @@ module Librarix
content_type 'application/json' content_type 'application/json'
end end
get '/movies' do namespace '/genres' do
get '' do
Librarix::Models::Genre.fetch.to_json
end
end
namespace '/movies' do
get '' do
Librarix::Filter.new(params).movies.to_json Librarix::Filter.new(params).movies.to_json
end end
post '/movies' do post '' do
id = json_params['tmdb_id'].to_i id = json_params['tmdb_id'].to_i
movie = Tmdb::Movie.detail(id) movie = Tmdb::Movie.detail(id)
@ -57,88 +50,28 @@ module Librarix
end end
end end
get '/movies/:id' do |id| get '/:id' do |id|
Librarix::Redis::Movie.new(id).fetch.to_json Librarix::Models::Movie.find(id: id).to_json
end end
get '/movies/:id/fetch' do |id| get '/:id/fetch' do |id|
Librarix::Redis::Movie.new(id).update Librarix::Models::Movie.fetch(85).to_json
end end
patch '/movies/:id/view' do |id| patch '/:id/view' do |id|
Librarix::Redis::Movie.new(params[:id]).view Librarix::Models::Movie.find(id: id).view!
{success: true}.to_json
end end
delete '/movies/:id' do |id| patch '/:id/download' do |id|
Librarix::Redis::Movie.new(params[:id]).remove Librarix::Models::Movie.find(id: id).download!
end {success: true}.to_json
end end
get '/' do delete '/:id' do |id|
slim :index, locals: {filter: Librarix::Filter.new(params)} Librarix::Models::Movie.find(id: id).destroy
{success: true}.to_json
end end
get '/movie/:id' do |id|
movie = Librarix::Redis::Movie.new(id).fetch
if request.xhr?
slim :'partials/movie', layout: false, locals: {movie: movie}
else
slim :movie, locals: {movie: movie}
end
end
get '/search' do
movies = if params['search'].nil?
Tmdb::Movie.popular.map { |m| Tmdb::Movie.new(m) }
elsif params['search'] == ''
[]
else
Tmdb::Movie.find(params['search'])
end
slim :search, locals: {movies: movies}
end
post '/add' do
id = params[:id].to_i
movie = Tmdb::Movie.detail(id)
if movie['status_code'] == 34
elsif Librarix::Redis::Movie.new(id).added?
else
Librarix::Redis::Movie.new(id).add
end
redirect to('/')
end
post '/update' do
movie = Librarix::Redis::Movie.new(params[:id]).update
if request.xhr?
slim :'partials/movie', layout: false, locals: {movie: movie}
else
redirect to('/')
end
end
post '/remove' do
Librarix::Redis::Movie.new(params[:id]).remove
if request.xhr?
""
else
redirect to('/')
end
end
post '/view' do
Librarix::Redis::Movie.new(params[:id]).view
if request.xhr?
""
else
redirect to('/')
end end
end end
end end

View File

@ -1,117 +0,0 @@
body {
margin: 10px;
}
a {
text-decoration: none;
color: #666;
&:hover {
color: #888;
}
}
h2 {
margin-top: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.2em;
font-family: Verdana,Arial,sans-serif;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
ul#menu {
display: flex;
flex-flow: row wrap;
justify-content: flex-end;
li {
margin: 5px;
}
}
#search-genres ul {
display: flex;
flex-flow: row wrap;
li {
margin-right: 0.5em;
}
}
ul.genres {
display: flex;
li {
margin-right: 0.4em;
}
li:last-child::after {
content: '';
}
li::after {
content: ',';
}
}
form #search-main {
display: flex;
input {
border-radius: 0.4em;
border: solid 3px #f4f4f4;
}
input[type="submit"] {
background-color: #f4f4f4;
}
input[type="text"] {
flex-grow: 1;
min-width: 50px;
font-size: 1.4em;
padding: 5px;
}
}
.default, .compact {
& > li {
padding: 10px 0;
}
& > li:nth-child(even) {
background-color: #f4f4f4;
}
.movie {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
.poster {
margin-right: 10px;
}
.informations {
align-self: flex-start;
flex: 1 1 300px;
}
}
}
.poster {
display: flex;
flex-flow: row wrap;
.movie {
width: 160px;
}
}

View File

@ -1,90 +0,0 @@
module Librarix
class Filter
def self.filter_by_genre(movies, genres)
return movies if genres.empty?
movies.reject do |movie|
(movie.genres.map { |genre| genre['name'] } & genres).empty?
end
end
def self.filter_by_title(movies, title)
return movies unless title
movies.select { |movie| movie.title.downcase.include?(title) }
end
def self.filter_by_view_state(movies, view_state)
if view_state == 'viewed'
movies.select(&:viewed?)
elsif view_state == 'not_viewed'
movies.reject(&:viewed?)
else
movies
end
end
def self.sort(movies, sort)
if sort == 'date'
movies.sort_by(&:release_date).reverse
else
movies.sort_by(&:title)
end
end
def self.group(movies, group, sort)
return {all: movies} unless group
if sort == 'date'
movies.group_by { |movie| movie.release_date.year }
else
movies.group_by { |movie| movie.title[0].upcase }
end
end
attr_reader :movies, :params
def initialize(params)
@params = params
end
def movies
@movies ||= begin
movies = Librarix::Redis::Movie.all
movies = self.class.filter_by_genre(movies, genres)
movies = self.class.filter_by_title(movies, params['title'])
movies = self.class.filter_by_view_state(movies, params['view_state'])
movies = self.class.sort(movies, sort)
movies = self.class.group(movies, group, sort)
end
end
def group
@group ||= params.key?('group') && params['group']
end
def genres
@genres ||= params.key?('genres') ? params['genres'].keys : []
end
def view_state
@view_state ||= if %w{all viewed not_viewed}.include?(params['view_state'])
params['view_state']
else
'all'
end
end
def sort
@sort ||= if %w{alphabetical date}.include?(params['sort'])
params['sort']
else
'date'
end
end
def maybe_search?
movies.all? { |k,v| v.empty? } && params.key?('title')
end
end
end

View File

@ -1,11 +0,0 @@
module Librarix
module Helpers
def template
if %w{default compact poster}.include?(params['template'])
params['template']
else
'default'
end
end
end
end

View File

@ -1,56 +0,0 @@
module Librarix
class Menu
module Helper
def menu
Librarix::Menu.menu.render request.path_info
end
end
def self.menu
@menu ||= new
end
attr_accessor :menu
def initialize
self.menu = []
end
def add(name, url)
self.menu << Element.new(name, url)
end
def render(path = nil)
around menu.map { |elem| elem.render path }.join
end
private
def around(content)
"<ul id=\"menu\">#{content}</ul>"
end
class Element
attr_accessor :name, :url
def initialize(name, url)
self.name = name
self.url = url
end
def render(path = nil)
around "<a href=\"#{url}\">#{name}</a>", path: path
end
def current?(path)
path == url
end
private
def around(element, path: nil)
"<li#{current?(path) ? ' class="active"' : ''}>#{element}</li>"
end
end
end
end

View File

@ -0,0 +1,5 @@
module Librarix::Models
end
require 'librarix/models/genre'
require 'librarix/models/movie'

View File

@ -0,0 +1,15 @@
class Librarix::Models::Genre < Sequel::Model
def self.fetch
unrestrict_primary_key
genres = Tmdb::Genre.list['genres']
genres.each do |genre|
find_or_create(id: genre['id']) { |g| g.name = genre['name'] }
end
genres
end
many_to_many :movies
plugin :association_dependencies, movies: :nullify
end

View File

@ -0,0 +1,37 @@
class Librarix::Models::Movie < Sequel::Model
def self.fetch(id)
unrestrict_primary_key
movie = find(id: id) || new(id: id)
movie.fetch!
end
plugin :serialization
serialize_attributes :json, :data
many_to_many :genres
plugin :association_dependencies, genres: :nullify
def fetch!
tmdb_data = Tmdb::Movie.detail(id)
self.data = tmdb_data
save
genres_id = data['genres'].map { |g| g['id'] }
Librarix::Models::Genre.where(id: genres_id).all.each do |genre|
add_genre(genre) unless genres.include?(genre)
end
self
end
def download!
self.downloaded = !downloaded
save
end
def view!
self.viewed = !viewed
save
end
end

View File

@ -1 +0,0 @@
body{margin:10px}a{text-decoration:none;color:#666}a:hover{color:#888}h2{margin-top:0;overflow:hidden;text-overflow:ellipsis;font-size:1.2em;font-family:Verdana,Arial,sans-serif}ul{list-style-type:none;margin:0;padding:0}ul#menu{display:flex;flex-flow:row wrap;justify-content:flex-end}ul#menu li{margin:5px}#search-genres ul{display:flex;flex-flow:row wrap}#search-genres ul li{margin-right:0.5em}ul.genres{display:flex}ul.genres li{margin-right:0.4em}ul.genres li:last-child::after{content:''}ul.genres li::after{content:','}form #search-main{display:flex}form #search-main input{border-radius:0.4em;border:solid 3px #f4f4f4}form #search-main input[type="submit"]{background-color:#f4f4f4}form #search-main input[type="text"]{flex-grow:1;min-width:50px;font-size:1.4em;padding:5px}.default>li,.compact>li{padding:10px 0}.default>li:nth-child(even),.compact>li:nth-child(even){background-color:#f4f4f4}.default .movie,.compact .movie{display:flex;flex-flow:row wrap;align-items:center;justify-content:center}.default .movie .poster,.compact .movie .poster{margin-right:10px}.default .movie .informations,.compact .movie .informations{align-self:flex-start;flex:1 1 300px}.poster{display:flex;flex-flow:row wrap}.poster .movie{width:160px}

View File

@ -1,82 +0,0 @@
function request(method, url, data, loadevent) {
var req = new XMLHttpRequest();
req.open(method, url);
req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
req.addEventListener('load', loadevent);
req.send(data)
}
function init() {
var movies = document.querySelectorAll('.movie');
for (var i = 0; i < movies.length; i++) {
new Movie(movies[i].dataset.id);
}
}
var Movie = function(id) {
this.id = parseInt(id);
this.element = document.querySelector('.movie[data-id="'+id+'"]');
var remove_button = this.element.querySelector('button[data-action="remove-movie"]');
var update_button = this.element.querySelector('button[data-action="update-movie"]');
var view_button = this.element.querySelector('button[data-action="view-movie"]');
if (remove_button) {
remove_button.addEventListener('click', function(e) {
e.preventDefault();
this.remove();
}.bind(this));
}
if (update_button) {
update_button.addEventListener('click', function(e) {
e.preventDefault();
this.update();
}.bind(this));
}
if (view_button) {
view_button.addEventListener('click', function(e) {
e.preventDefault();
this.view();
}.bind(this));
}
this.formData = function() {
var data = new FormData();
data.append('id', this.id);
return data;
}
this.update = function() {
request('POST', '/update', this.formData(), function(e) {
oldelem = this.element;
tmp_elem = document.createElement('div');
tmp_elem.innerHTML = e.target.response;
this.element = tmp_elem.firstChild;
oldelem.parentNode.replaceChild(this.element, oldelem);
}.bind(this));
};
this.remove = function() {
request('POST', '/remove', this.formData(), function(e) {
this.element.parentNode.remove();
}.bind(this));
};
this.view = function() {
request('POST', '/view', this.formData(), function(e) {
this.element.querySelector('button[data-action="view-movie"]').parentNode.remove();
}.bind(this));
};
};
document.addEventListener('DOMContentLoaded', function(event) {
init();
});

View File

@ -1,49 +0,0 @@
require 'redis'
module Librarix
class Redis
TTL = 10800
attr_reader :connection, :namespace
def initialize(connection, namespace:)
@connection = connection
@namespace = namespace
end
def exist?(key)
exists key
end
def keys(pattern)
@connection.keys("#{prefix}#{pattern}").map { |key| key.sub(prefix, '') }
end
def fetch(key, options = {})
if options[:force] || (!exist?(key) && block_given?)
value = yield
set key, value
expire key, TTL if options.key?(:ttl) && options[:ttl]
value
else
get key
end
end
def respond_to?(name, include_all = false)
super || @connection.respond_to?(name, include_all)
end
def method_missing(name, *args, &block)
return super unless @connection.respond_to?(name)
@connection.send(name, "#{prefix}#{args.shift}", *args)
end
private
def prefix
"#{namespace}:"
end
end
end

View File

@ -1,64 +0,0 @@
module Librarix
class Redis
class Movie
attr_reader :id
def self.all
Array(Librarix.redis.smembers('movies_id')).map { |id| new(id).fetch }
end
def self.genres
fetch_genres unless Librarix.redis.exists('genres')
Librarix.redis.smembers('genres').sort
end
def self.fetch_genres
Tmdb::Genre.list['genres'].each do |genre|
Librarix.redis.sadd('genres', genre['name'])
end
end
def initialize(id)
@id = id.to_i
end
def movie
@movie ||= fetch
end
def add
Librarix.redis.sadd('movies_id', id)
movie
end
def added?
Librarix.redis.sismember('movies_id', id)
end
def fetch(force = false)
data = JSON.parse(Librarix.redis.fetch("movie:#{id}", force: force) do
Tmdb::Movie.detail(id).to_json
end)
Tmdb::Movie.new(data)
end
def update
fetch(true)
end
def remove
Librarix.redis.del("movie:#{id}")
Librarix.redis.srem('movies_id', id)
end
def view
Librarix.redis.sadd('viewed_movies_id', id)
end
def viewed?
Librarix.redis.sismember('viewed_movies_id', id)
end
end
end
end

View File

@ -1,41 +0,0 @@
require 'themoviedb'
module Librarix
module TheMovieDB
def poster_url(path, size = 'w92')
base_url = Librarix.redis.fetch('base_url', ttl: true) do
Tmdb::Configuration.new.base_url
end
"#{base_url}#{size}#{path}"
end
def movie(id)
Librarix::Redis::Movie.new(id).fetch
end
module Movie
def genres
super || []
end
def release_date
@_release_date ||= Date.parse(super) unless super.nil? || super.empty?
end
def release_year
release_date.year unless release_date.nil?
end
def added?
Librarix::Redis::Movie.new(id).added?
end
def viewed?
Librarix::Redis::Movie.new(id).viewed?
end
end
end
end
Tmdb::Movie.prepend Librarix::TheMovieDB::Movie

View File

@ -1,39 +0,0 @@
- content_for :title do
| Movies
form method="get" action="/" id="filter"
div id="search-main"
input type="text" name="title" value="#{params['title']}" autocomplete="off"
input type="submit" value="Filter"
div id="search-more"
select name="view_state"
option value="all" selected=(filter.view_state == 'all') All movies
option value="viewed" selected=(filter.view_state == 'viewed') Only viewed
option value="not_viewed" selected=(filter.view_state == 'not_viewed') Not viewed
select name="sort"
option value="alphabetical" selected=(filter.sort == 'alphabetical') Alphabetical
option value="date" selected=(filter.sort == 'date') Release date
input type="checkbox" name="group" id="group" checked=(filter.group)
label for="group" Group by sort
select name="template"
option value="default" selected=(template == 'default') Default
option value="compact" selected=(template == 'compact') Compact
option value="poster" selected=(template == 'poster') Poster
div id="search-genres"
ul
- Librarix::Redis::Movie.genres.each do |genre|
li
input type="checkbox" name="genres[#{genre}]" id="#{genre}" checked=(filter.genres.include?(genre))
label for="#{genre}" = genre
- filter.movies.each do |group, movies|
- unless group == :all
h1 = group
== slim :'partials/list', locals: {movies: movies}
- if filter.maybe_search?
p
| Search for
strong
a<> href="#{url("/search?search=#{params['title']}")}" = params['title']
| ?

View File

@ -1,13 +0,0 @@
doctype html
html
head
title
= yield_content :title
| - Librarix
meta charset="utf-8"
meta name="viewport" content="initial-scale=1.0, user-scalable=yes"
link rel="stylesheet" media="all" href="/application.css"
script type="text/javascript" src="/application.js"
body
== menu
== yield

View File

@ -1,27 +0,0 @@
- content_for :title do
= movie.title
.movie data-id="#{movie.id}"
h1 = movie.title
.poster
- if movie.poster_path
img src="#{poster_url(movie.poster_path, 'w154')}"
.informations
p = movie.release_date
p = movie.overview
p
a> href="https://www.themoviedb.org/movie/#{movie.id}" The Movie DB
| (#{movie.vote_average}/10, #{movie.vote_count} votes)
.actions
- if movie.added?
- unless movie.viewed?
form method="post" action="/view"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="view-movie" View
form method="post" action="/remove"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="remove-movie" Remove
- else
form method="post" action="/add"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="add-movie" Add

View File

@ -1,4 +0,0 @@
ul.movies class="#{template}"
- movies.each do |movie|
li
== slim :'partials/movie', locals: {movie: movie}

View File

@ -1,2 +0,0 @@
.movie data-id="#{movie.id}"
== slim :"partials/movie/#{template}", locals: {movie: movie}

View File

@ -1,19 +0,0 @@
.poster
- if movie.poster_path
img src="#{poster_url(movie.poster_path, 'w92')}"
.informations
h2
a href="#{url("/movie/#{movie.id}")}" #{movie.title}
.actions
- if movie.added?
- unless movie.viewed?
form method="post" action="/view"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="view-movie" View
form method="post" action="/remove"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="remove-movie" Remove
- else
form method="post" action="/add"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="add-movie" Add

View File

@ -1,27 +0,0 @@
.poster
- if movie.poster_path
img src="#{poster_url(movie.poster_path, 'w154')}"
.informations
h2
a href="#{url("/movie/#{movie.id}")}" #{movie.title}
p = movie.release_date
p = movie.overview
ul.genres
- movie.genres.each do |genre|
li = genre['name']
.actions
- if movie.added?
- unless movie.viewed?
form method="post" action="/view"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="view-movie" View
form method="post" action="/update"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="update-movie" Update
form method="post" action="/remove"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="remove-movie" Remove
- else
form method="post" action="/add"
input type="hidden" name="id" value="#{movie.id}"
button type="submit" data-action="add-movie" Add

View File

@ -1,7 +0,0 @@
.poster
- if movie.poster_path
a href="#{url("/movie/#{movie.id}")}"
img src="#{poster_url(movie.poster_path, 'w154')}"
.informations
h2
a href="#{url("/movie/#{movie.id}")}" #{movie.title}

View File

@ -1,10 +0,0 @@
- content_for :title do
| Search
form method="get" action="/search" id="search"
div id="search-main"
input type="text" name="search" value="#{params['search']}" autocomplete="off" autofocus="on"
input type="submit" value="Search"
div
== slim :'partials/list', locals: {movies: movies}

View File

@ -20,11 +20,11 @@ Gem::Specification.new do |spec|
spec.add_dependency "sinatra", '~> 2.0' spec.add_dependency "sinatra", '~> 2.0'
spec.add_dependency "sinatra-contrib", '~> 2.0' spec.add_dependency "sinatra-contrib", '~> 2.0'
spec.add_dependency "slim", '~> 3.0' spec.add_dependency "sequel", '~> 5.21'
spec.add_dependency "redis", '~> 3.2' spec.add_dependency "sqlite3", '~> 1.4'
spec.add_dependency "themoviedb", '~> 0.1' spec.add_dependency "themoviedb", '~> 1.0'
spec.add_development_dependency "bundler", "~> 2.0" spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "sass", "~> 3.4" spec.add_development_dependency "irb"
end end