forked from midou/invidious
		
	Add user preferences
This commit is contained in:
		| @@ -21,6 +21,10 @@ body { | ||||
|   color: #f0f0f0; | ||||
| } | ||||
|  | ||||
| .pure-form > fieldset > input { | ||||
| .pure-form > fieldset > input, .pure-control-group > input { | ||||
|   color: #101010; | ||||
| } | ||||
|  | ||||
| .pure-form > fieldset > select, .pure-control-group > select { | ||||
|   color: #101010; | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ CREATE TABLE public.users | ||||
|     notifications text[] COLLATE pg_catalog."default", | ||||
|     subscriptions text[] COLLATE pg_catalog."default", | ||||
|     email text COLLATE pg_catalog."default" NOT NULL, | ||||
|     preferences text COLLAGE pg_catalog."default", | ||||
|     CONSTRAINT users_email_key UNIQUE (email), | ||||
|     CONSTRAINT users_id_key UNIQUE (id) | ||||
| ) | ||||
|   | ||||
							
								
								
									
										151
									
								
								src/invidious.cr
									
									
									
									
									
								
							
							
						
						
									
										151
									
								
								src/invidious.cr
									
									
									
									
									
								
							| @@ -214,24 +214,16 @@ spawn do | ||||
| end | ||||
|  | ||||
| before_all do |env| | ||||
|   if env.request.cookies.has_key?("SID") | ||||
|     env.set "authorized", true | ||||
|   if env.request.cookies.has_key? "SID" | ||||
|     headers = HTTP::Headers.new | ||||
|     headers["Cookie"] = env.request.headers["Cookie"] | ||||
|  | ||||
|     sid = env.request.cookies["SID"].value | ||||
|     env.set "sid", sid | ||||
|  | ||||
|     subscriptions = PG_DB.query_one?("SELECT subscriptions FROM users WHERE id = $1", sid, as: Array(String)) | ||||
|     subscriptions ||= [] of String | ||||
|     env.set "subscriptions", subscriptions | ||||
|     client = make_client(YT_URL) | ||||
|     user = get_user(sid, client, headers, PG_DB, false) | ||||
|  | ||||
|     notifications = PG_DB.query_one?("SELECT cardinality(notifications) FROM users WHERE id = $1", sid, as: Int32) | ||||
|     notifications ||= 0 | ||||
|  | ||||
|     env.set "notifications", notifications | ||||
|   end | ||||
|  | ||||
|   if env.request.cookies.has_key?("darktheme") && env.request.cookies["darktheme"].value == "true" | ||||
|     env.set "darktheme", true | ||||
|     env.set "user", user | ||||
|   end | ||||
| end | ||||
|  | ||||
| @@ -240,9 +232,11 @@ get "/" do |env| | ||||
| end | ||||
|  | ||||
| get "/watch" do |env| | ||||
|   authorized = env.get? "authorized" | ||||
|   if authorized | ||||
|     subscriptions = env.get("subscriptions").as(Array(String)) | ||||
|   user = env.get? "user" | ||||
|   if user | ||||
|     user = user.as(User) | ||||
|     preferences = user.preferences | ||||
|     subscriptions = user.subscriptions.as(Array(String)) | ||||
|   end | ||||
|   subscriptions ||= [] of String | ||||
|  | ||||
| @@ -670,11 +664,73 @@ get "/signout" do |env| | ||||
|   env.redirect referer | ||||
| end | ||||
|  | ||||
| get "/preferences" do |env| | ||||
|   user = env.get? "user" | ||||
|  | ||||
|   referer = env.request.headers["referer"]? | ||||
|   referer ||= "/preferences" | ||||
|  | ||||
|   if referer.size > 64 | ||||
|     puts "nope" | ||||
|     referer = "/preferences" | ||||
|   end | ||||
|  | ||||
|   if user | ||||
|     user = user.as(User) | ||||
|     templated "preferences" | ||||
|   else | ||||
|     env.redirect referer | ||||
|   end | ||||
| end | ||||
|  | ||||
| post "/preferences" do |env| | ||||
|   user = env.get? "user" | ||||
|  | ||||
|   referer = env.params.query["referer"]? | ||||
|   referer ||= "/preferences" | ||||
|  | ||||
|   if user | ||||
|     user = user.as(User) | ||||
|  | ||||
|     video_loop = env.params.body["video_loop"]?.try &.as(String) | ||||
|     video_loop ||= "off" | ||||
|     video_loop = video_loop == "on" | ||||
|  | ||||
|     autoplay = env.params.body["autoplay"]?.try &.as(String) | ||||
|     autoplay ||= "off" | ||||
|     autoplay = autoplay == "on" | ||||
|  | ||||
|     quality = env.params.body["quality"]?.try &.as(String) | ||||
|     quality ||= "hd720" | ||||
|  | ||||
|     volume = env.params.body["volume"]?.try &.as(String).to_i | ||||
|     volume ||= 100 | ||||
|  | ||||
|     dark_mode = env.params.body["dark_mode"]?.try &.as(String) | ||||
|     dark_mode ||= "off" | ||||
|     dark_mode = dark_mode == "on" | ||||
|  | ||||
|     preferences = { | ||||
|       "video_loop" => video_loop, | ||||
|       "autoplay"   => autoplay, | ||||
|       "quality"    => quality, | ||||
|       "volume"     => volume, | ||||
|       "dark_mode"  => dark_mode, | ||||
|     }.to_json | ||||
|  | ||||
|     PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email) | ||||
|   end | ||||
|  | ||||
|   env.redirect referer | ||||
| end | ||||
|  | ||||
| # Get subscriptions for authorized user | ||||
| get "/feed/subscriptions" do |env| | ||||
|   authorized = env.get? "authorized" | ||||
|   user = env.get? "user" | ||||
|  | ||||
|   if user | ||||
|     user = user.as(User) | ||||
|  | ||||
|   if authorized | ||||
|     max_results = env.params.query["maxResults"]?.try &.to_i || 40 | ||||
|  | ||||
|     page = env.params.query["page"]?.try &.to_i | ||||
| @@ -688,14 +744,6 @@ get "/feed/subscriptions" do |env| | ||||
|       offset = (page - 1) * max_results | ||||
|     end | ||||
|  | ||||
|     headers = HTTP::Headers.new | ||||
|     headers["Cookie"] = env.request.headers["Cookie"] | ||||
|  | ||||
|     sid = env.get("sid").as(String) | ||||
|  | ||||
|     client = make_client(YT_URL) | ||||
|     user = get_user(sid, client, headers, PG_DB) | ||||
|  | ||||
|     args = arg_array(user.subscriptions, 3) | ||||
|     videos = PG_DB.query_all("SELECT * FROM channel_videos WHERE ucid IN (#{args}) \ | ||||
|     ORDER BY published DESC LIMIT $1 OFFSET $2", [limit, offset] + user.subscriptions, as: ChannelVideo) | ||||
| @@ -709,7 +757,7 @@ get "/feed/subscriptions" do |env| | ||||
|       videos = videos[0..max_results] | ||||
|     end | ||||
|  | ||||
|     PG_DB.exec("UPDATE users SET notifications = $1 WHERE id = $2", [] of String, sid) | ||||
|     PG_DB.exec("UPDATE users SET notifications = $1 WHERE id = $2", [] of String, user.id) | ||||
|     env.set "notifications", 0 | ||||
|  | ||||
|     templated "subscriptions" | ||||
| @@ -726,12 +774,14 @@ end | ||||
| # /modify_notifications?receive_all_updates=false&receive_no_updates=false | ||||
| # will "unding" all subscriptions. | ||||
| get "/modify_notifications" do |env| | ||||
|   authorized = env.get? "authorized" | ||||
|   user = env.get? "user" | ||||
|  | ||||
|   referer = env.request.headers["referer"]? | ||||
|   referer ||= "/" | ||||
|  | ||||
|   if authorized | ||||
|   if user | ||||
|     user = user.as(User) | ||||
|  | ||||
|     channel_req = {} of String => String | ||||
|  | ||||
|     channel_req["receive_all_updates"] = env.params.query["receive_all_updates"]? || "true" | ||||
| @@ -770,12 +820,14 @@ get "/modify_notifications" do |env| | ||||
| end | ||||
|  | ||||
| get "/subscription_manager" do |env| | ||||
|   authorized = env.get? "authorized" | ||||
|   if !authorized | ||||
|   user = env.get? "user" | ||||
|  | ||||
|   if !user | ||||
|     next env.redirect "/" | ||||
|   end | ||||
|  | ||||
|   subscriptions = env.get?("subscriptions").as(Array(String)) | ||||
|   user = user.as(User) | ||||
|   subscriptions = user.subscriptions | ||||
|   subscriptions ||= [] of String | ||||
|  | ||||
|   client = make_client(YT_URL) | ||||
| @@ -788,11 +840,13 @@ get "/subscription_manager" do |env| | ||||
| end | ||||
|  | ||||
| get "/subscription_ajax" do |env| | ||||
|   authorized = env.get? "authorized" | ||||
|   user = env.get? "user" | ||||
|   referer = env.request.headers["referer"]? | ||||
|   referer ||= "/" | ||||
|  | ||||
|   if authorized | ||||
|   if user | ||||
|     user = user.as(User) | ||||
|  | ||||
|     if env.params.query["action_create_subscription_to_channel"]? | ||||
|       action = "action_create_subscription_to_channel" | ||||
|     elsif env.params.query["action_remove_subscriptions"]? | ||||
| @@ -827,7 +881,7 @@ get "/subscription_ajax" do |env| | ||||
|  | ||||
|     # Update user | ||||
|     if client.post(post_url, headers, post_req).status_code == 200 | ||||
|       sid = env.get("sid").as(String) | ||||
|       sid = user.id | ||||
|  | ||||
|       case action | ||||
|       when .starts_with? "action_create" | ||||
| @@ -847,11 +901,10 @@ get "/user/:user" do |env| | ||||
| end | ||||
|  | ||||
| get "/channel/:ucid" do |env| | ||||
|   authorized = env.get? "authorized" | ||||
|   if authorized | ||||
|     sid = env.get("sid").as(String) | ||||
|  | ||||
|     subscriptions = PG_DB.query_one?("SELECT subscriptions FROM users WHERE id = $1", sid, as: Array(String)) | ||||
|   user = env.get? "user" | ||||
|   if user | ||||
|     user = user.as(User) | ||||
|     subscriptions = user.subscriptions | ||||
|   end | ||||
|   subscriptions ||= [] of String | ||||
|  | ||||
| @@ -889,20 +942,6 @@ get "/channel/:ucid" do |env| | ||||
|   templated "channel" | ||||
| end | ||||
|  | ||||
| get "/modify_theme" do |env| | ||||
|   referer = env.request.headers["referer"]? | ||||
|   referer ||= "/" | ||||
|  | ||||
|   if env.params.query["dark"]? | ||||
|     env.response.cookies["darktheme"] = "true" | ||||
|   elsif env.params.query["light"]? | ||||
|     env.request.cookies["darktheme"].expires = Time.new(1990, 1, 1) | ||||
|     env.request.cookies.add_response_headers(env.response.headers) | ||||
|   end | ||||
|  | ||||
|   env.redirect referer | ||||
| end | ||||
|  | ||||
| get "/redirect" do |env| | ||||
|   if env.params.query["q"]? | ||||
|     env.redirect env.params.query["q"] | ||||
| @@ -1158,6 +1197,6 @@ end | ||||
| public_folder "assets" | ||||
|  | ||||
| add_handler FilteredCompressHandler.new | ||||
| add_context_storage_type(Array(String)) | ||||
| add_context_storage_type(User) | ||||
|  | ||||
| Kemal.run | ||||
|   | ||||
| @@ -17,6 +17,14 @@ macro rendered(filename) | ||||
|   render "src/invidious/views/#{{{filename}}}.ecr" | ||||
| end | ||||
|  | ||||
| DEFAULT_USER_PREFERENCES = Preferences.from_json({ | ||||
|   "video_loop" => false, | ||||
|   "autoplay"   => false, | ||||
|   "quality"    => "hd720", | ||||
|   "volume"     => 100, | ||||
|   "dark_mode"  => false, | ||||
| }.to_json) | ||||
|  | ||||
| class Config | ||||
|   YAML.mapping({ | ||||
|     crawl_threads:   Int32, | ||||
| @@ -86,12 +94,6 @@ class Video | ||||
| end | ||||
|  | ||||
| class InvidiousChannel | ||||
|   module XMLConverter | ||||
|     def self.from_rs(rs) | ||||
|       XML.parse_html(rs.read(String)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   add_mapping({ | ||||
|     id:      String, | ||||
|     author:  String, | ||||
| @@ -111,12 +113,33 @@ class ChannelVideo | ||||
| end | ||||
|  | ||||
| class User | ||||
|   module PreferencesConverter | ||||
|     def self.from_rs(rs) | ||||
|       Preferences.from_json(rs.read(String)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   add_mapping({ | ||||
|     id:            String, | ||||
|     updated:       Time, | ||||
|     notifications: Array(String), | ||||
|     subscriptions: Array(String), | ||||
|     email:         String, | ||||
|     preferences:   { | ||||
|       type:      Preferences, | ||||
|       default:   DEFAULT_USER_PREFERENCES, | ||||
|       converter: PreferencesConverter, | ||||
|     }, | ||||
|   }) | ||||
| end | ||||
|  | ||||
| class Preferences | ||||
|   JSON.mapping({ | ||||
|     video_loop: Bool, | ||||
|     autoplay:   Bool, | ||||
|     quality:    String, | ||||
|     volume:     Int32, | ||||
|     dark_mode:  Bool, | ||||
|   }) | ||||
| end | ||||
|  | ||||
| @@ -674,11 +697,11 @@ def fetch_channel(ucid, client, db, pull_all_videos = true) | ||||
|   return channel | ||||
| end | ||||
|  | ||||
| def get_user(sid, client, headers, db) | ||||
| def get_user(sid, client, headers, db, refresh = true) | ||||
|   if db.query_one?("SELECT EXISTS (SELECT true FROM users WHERE id = $1)", sid, as: Bool) | ||||
|     user = db.query_one("SELECT * FROM users WHERE id = $1", sid, as: User) | ||||
|  | ||||
|     if Time.now - user.updated > 1.minute | ||||
|     if refresh && Time.now - user.updated > 1.minute | ||||
|       user = fetch_user(sid, client, headers, db) | ||||
|       user_array = user.to_a | ||||
|       args = arg_array(user_array) | ||||
| @@ -723,7 +746,7 @@ def fetch_user(sid, client, headers, db) | ||||
|     email = "" | ||||
|   end | ||||
|  | ||||
|   user = User.new(sid, Time.now, [] of String, channels, email) | ||||
|   user = User.new(sid, Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES) | ||||
|   return user | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| <% end %> | ||||
|  | ||||
| <h1><%= author %></h1> | ||||
| <% if authorized %> | ||||
| <% if user %> | ||||
|     <% if subscriptions.includes? ucid %> | ||||
|     <p> | ||||
|         <a href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>"> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|   <link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/grids-responsive-min.css"> | ||||
|   <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.6/css/all.css"> | ||||
|   <link rel="stylesheet" href="/css/default.css"> | ||||
|   <% if env.get? "darktheme" %> | ||||
|   <% if env.get?("user") && env.get("user").as(User).preferences.dark_mode %> | ||||
|   <link rel="stylesheet" href="/css/darktheme.css"> | ||||
|   <% else %> | ||||
|   <link rel="stylesheet" href="/css/lighttheme.css"> | ||||
| @@ -21,46 +21,46 @@ | ||||
|     <div class="pure-u-1 pure-u-md-1-5"></div> | ||||
|     <div class="pure-u-1 pure-u-md-3-5"> | ||||
|       <div class="pure-g" style="padding:1em;"> | ||||
|         <div class="pure-u-1 pure-u-md-1-5"> | ||||
|         <div class="pure-u-1 pure-u-md-4-24"> | ||||
|           <center><a href="/" class="pure-menu-heading">Invidious</a></center> | ||||
|         </div> | ||||
|         <div class="pure-u-1 pure-u-md-3-5"> | ||||
|         <div class="pure-u-1 pure-u-md-12-24"> | ||||
|           <form class="pure-form" action="/search" method="get"> | ||||
|             <fieldset> | ||||
|               <input type="search" style="width:100%;" name="q" placeholder="search" value="<%= env.params.query["q"]? %>"> | ||||
|             </fieldset> | ||||
|           </form> | ||||
|         </div> | ||||
|         <div class="pure-u-1 pure-u-md-1-5"> | ||||
|         <% if env.get? "authorized" %> | ||||
|         <div class="pure-g"> | ||||
|           <div class="pure-u-1-3"> | ||||
|             <a href="/feed/subscriptions" class="pure-menu-heading"> | ||||
|               <% notifications = env.get("notifications").as(Int32) %> | ||||
|               <% if notifications > 0 %> | ||||
|               <center><%= notifications %> <i class="fas fa-bell"></i></center> | ||||
|               <% else %> | ||||
|               <center><i class="far fa-bell"></i></center> | ||||
|               <% end %> | ||||
|             </a> | ||||
|         <div class="pure-u-1 pure-u-md-8-24"> | ||||
|         <% if env.get? "user" %> | ||||
|           <div class="pure-g"> | ||||
|             <div class="pure-u-1-3"> | ||||
|               <a href="/feed/subscriptions" class="pure-menu-heading"> | ||||
|                 <% notifications = env.get("user").as(User).notifications.size %> | ||||
|                 <% if notifications > 0 %> | ||||
|                 <center><%= notifications %> <i class="fas fa-bell"></i></center> | ||||
|                 <% else %> | ||||
|                 <center><i class="far fa-bell"></i></center> | ||||
|                 <% end %> | ||||
|               </a> | ||||
|             </div> | ||||
|             <div class="pure-u-1-3"> | ||||
|               <a href="/preferences" class="pure-menu-heading"> | ||||
|                 <center><i class="fas fa-cog"></i></center> | ||||
|               </a> | ||||
|             </div> | ||||
|             <div class="pure-u-1-3"> | ||||
|               <a href="/signout" class="pure-menu-heading"> | ||||
|                 <center>Sign out</center> | ||||
|               </a> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="pure-u-2-3"> | ||||
|               <center><a href="/signout" class="pure-menu-heading">Sign out</a></center> | ||||
|           </div> | ||||
|         </div> | ||||
|         <% else %> | ||||
|           <center><a href="/login" class="pure-menu-heading">Login</a></center> | ||||
|         <% end %> | ||||
|         </div> | ||||
|       </div> | ||||
|       <%= content %> | ||||
|       <center class="h-box"> | ||||
|         <% if env.get? "darktheme" %> | ||||
|         <a href="/modify_theme?light">Light theme</a> | ||||
|         <% else %> | ||||
|         <a href="/modify_theme?dark">Dark theme</a> | ||||
|         <% end %> | ||||
|       </center> | ||||
|       <center class="h-box"> | ||||
|         Released under AGPLv3 by <a href="https://github.com/omarroth">Omar Roth</a> - | ||||
|         source available <a href="https://github.com/omarroth/invidious">here</a> | ||||
|   | ||||
							
								
								
									
										50
									
								
								src/invidious/views/preferences.ecr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/invidious/views/preferences.ecr
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| <% content_for "header" do %> | ||||
| <title>Preferences - Invidious</title> | ||||
| <% end %> | ||||
|  | ||||
| <script> | ||||
| function update_value(element) { | ||||
|     document.getElementById('volume-value').innerText = element.value; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <form class="pure-form pure-form-aligned" action="/preferences?referer=<%= referer %>" method="post"> | ||||
|     <fieldset> | ||||
|         <legend>Player preferences</legend> | ||||
|  | ||||
|         <div class="pure-control-group"> | ||||
|             <label for="video_loop">Always loop: </label> | ||||
|             <input name="video_loop" id="video_loop" type="checkbox" <% if user.preferences.video_loop %>checked<% end %>> | ||||
|         </div> | ||||
|  | ||||
|         <div class="pure-control-group"> | ||||
|             <label for="autoplay">Autoplay: </label> | ||||
|             <input name="autoplay" id="autoplay" type="checkbox" <% if user.preferences.autoplay %>checked<% end %>> | ||||
|         </div> | ||||
|  | ||||
|         <div class="pure-control-group"> | ||||
|             <label for="quality">Preferred video quality: </label> | ||||
|             <select name="quality" id="quality" selected="<%= user.preferences.quality %>"> | ||||
|                 <option>hd720</option> | ||||
|                 <option>medium</option> | ||||
|                 <option>small</option> | ||||
|             </select> | ||||
|         </div> | ||||
|  | ||||
|         <div class="pure-control-group"> | ||||
|             <label for="volume">Player volume: </label> | ||||
|             <input name="volume" id="volume" oninput="update_value(this);" type="range" min="0" max="100" step="5" value="<%= user.preferences.volume %>"> | ||||
|             <span class="pure-form-message-inline" id="volume-value"><%= user.preferences.volume %></span> | ||||
|         </div> | ||||
|  | ||||
|         <legend>Visual preferences</legend> | ||||
|         <div class="pure-control-group"> | ||||
|             <label for="dark_mode">Dark mode: </label> | ||||
|             <input name="dark_mode" id="dark_mode" type="checkbox" <% if user.preferences.dark_mode %>checked<% end %>> | ||||
|         </div> | ||||
|  | ||||
|         <div class="pure-controls"> | ||||
|             <button type="submit" class="pure-button pure-button-primary">Save preferences</button> | ||||
|         </div> | ||||
|     </fieldset> | ||||
| </form> | ||||
| @@ -17,8 +17,12 @@ | ||||
|             <% end %> | ||||
|         <% else %> | ||||
|             <% fmt_stream.each_with_index do |fmt, i| %> | ||||
|                 <% if preferences %> | ||||
|                 <source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= preferences.quality == fmt["label"].split(" - ")[0] %>"> | ||||
|                 <% else %> | ||||
|                 <source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= i == 0 ? true : false %>"> | ||||
|             <% end %>     | ||||
|                 <% end %> | ||||
|             <% end %> | ||||
|         <% end %> | ||||
|     </video> | ||||
| </div> | ||||
| @@ -26,6 +30,10 @@ | ||||
| <script> | ||||
| var options = { | ||||
|     preload: "auto", | ||||
|     <% if preferences %> | ||||
|     <% if preferences.autoplay %>autoplay: true, <% end %> | ||||
|     <% if preferences.video_loop %>loop: true, <% end %> | ||||
|     <% end %> | ||||
|     playbackRates: [0.5, 1, 1.5, 2], | ||||
|     controlBar: { | ||||
|       children: [ | ||||
| @@ -81,6 +89,9 @@ var player = videojs('player', options, function() { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| <% if preferences %> | ||||
| player.volume(<%= preferences.volume.to_f / 100 %>); | ||||
| <% end %> | ||||
| player.offset({ | ||||
|   start: <%= video_start %>, | ||||
|   end: <%= video_end %> | ||||
| @@ -165,7 +176,7 @@ player.src(currentSources); | ||||
|                     <h3><%= video.author %></h3> | ||||
|                 </a> | ||||
|             </p> | ||||
|         <% if authorized %> | ||||
|         <% if user %> | ||||
|             <% if subscriptions.includes? video.ucid %> | ||||
|             <p> | ||||
|                 <a href="/subscription_ajax?action_remove_subscriptions=1&c=<%= video.ucid %>"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user