spotify web setup is done
							parent
							
								
									b368a750fe
								
							
						
					
					
						commit
						5eaad428ff
					
				
							
								
								
									
										4
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										4
									
								
								Gemfile
								
								
								
								
							| 
						 | 
					@ -54,3 +54,7 @@ group :test do
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem "devise", "~> 4.9"
 | 
					gem "devise", "~> 4.9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gem "rest-client", "~> 2.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gem "dotenv", "~> 3.1"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										17
									
								
								Gemfile.lock
								
								
								
								
							| 
						 | 
					@ -107,6 +107,8 @@ GEM
 | 
				
			||||||
      railties (>= 4.1.0)
 | 
					      railties (>= 4.1.0)
 | 
				
			||||||
      responders
 | 
					      responders
 | 
				
			||||||
      warden (~> 1.2.3)
 | 
					      warden (~> 1.2.3)
 | 
				
			||||||
 | 
					    domain_name (0.6.20240107)
 | 
				
			||||||
 | 
					    dotenv (3.1.8)
 | 
				
			||||||
    drb (2.2.3)
 | 
					    drb (2.2.3)
 | 
				
			||||||
    erb (5.0.1)
 | 
					    erb (5.0.1)
 | 
				
			||||||
    erubi (1.13.1)
 | 
					    erubi (1.13.1)
 | 
				
			||||||
| 
						 | 
					@ -117,6 +119,9 @@ GEM
 | 
				
			||||||
      raabro (~> 1.4)
 | 
					      raabro (~> 1.4)
 | 
				
			||||||
    globalid (1.2.1)
 | 
					    globalid (1.2.1)
 | 
				
			||||||
      activesupport (>= 6.1)
 | 
					      activesupport (>= 6.1)
 | 
				
			||||||
 | 
					    http-accept (1.7.0)
 | 
				
			||||||
 | 
					    http-cookie (1.0.8)
 | 
				
			||||||
 | 
					      domain_name (~> 0.5)
 | 
				
			||||||
    i18n (1.14.7)
 | 
					    i18n (1.14.7)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
    importmap-rails (2.1.0)
 | 
					    importmap-rails (2.1.0)
 | 
				
			||||||
| 
						 | 
					@ -145,6 +150,10 @@ GEM
 | 
				
			||||||
      net-smtp
 | 
					      net-smtp
 | 
				
			||||||
    marcel (1.0.4)
 | 
					    marcel (1.0.4)
 | 
				
			||||||
    matrix (0.4.3)
 | 
					    matrix (0.4.3)
 | 
				
			||||||
 | 
					    mime-types (3.7.0)
 | 
				
			||||||
 | 
					      logger
 | 
				
			||||||
 | 
					      mime-types-data (~> 3.2025, >= 3.2025.0507)
 | 
				
			||||||
 | 
					    mime-types-data (3.2025.0617)
 | 
				
			||||||
    mini_mime (1.1.5)
 | 
					    mini_mime (1.1.5)
 | 
				
			||||||
    minitest (5.25.5)
 | 
					    minitest (5.25.5)
 | 
				
			||||||
    msgpack (1.8.0)
 | 
					    msgpack (1.8.0)
 | 
				
			||||||
| 
						 | 
					@ -157,6 +166,7 @@ GEM
 | 
				
			||||||
      timeout
 | 
					      timeout
 | 
				
			||||||
    net-smtp (0.5.1)
 | 
					    net-smtp (0.5.1)
 | 
				
			||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
 | 
					    netrc (0.11.0)
 | 
				
			||||||
    nio4r (2.7.4)
 | 
					    nio4r (2.7.4)
 | 
				
			||||||
    nokogiri (1.18.8-aarch64-linux-gnu)
 | 
					    nokogiri (1.18.8-aarch64-linux-gnu)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
| 
						 | 
					@ -245,6 +255,11 @@ GEM
 | 
				
			||||||
    responders (3.1.1)
 | 
					    responders (3.1.1)
 | 
				
			||||||
      actionpack (>= 5.2)
 | 
					      actionpack (>= 5.2)
 | 
				
			||||||
      railties (>= 5.2)
 | 
					      railties (>= 5.2)
 | 
				
			||||||
 | 
					    rest-client (2.1.0)
 | 
				
			||||||
 | 
					      http-accept (>= 1.7.0, < 2.0)
 | 
				
			||||||
 | 
					      http-cookie (>= 1.0.2, < 2.0)
 | 
				
			||||||
 | 
					      mime-types (>= 1.16, < 4.0)
 | 
				
			||||||
 | 
					      netrc (~> 0.8)
 | 
				
			||||||
    rexml (3.4.1)
 | 
					    rexml (3.4.1)
 | 
				
			||||||
    rubocop (1.77.0)
 | 
					    rubocop (1.77.0)
 | 
				
			||||||
      json (~> 2.3)
 | 
					      json (~> 2.3)
 | 
				
			||||||
| 
						 | 
					@ -339,12 +354,14 @@ DEPENDENCIES
 | 
				
			||||||
  capybara
 | 
					  capybara
 | 
				
			||||||
  debug
 | 
					  debug
 | 
				
			||||||
  devise (~> 4.9)
 | 
					  devise (~> 4.9)
 | 
				
			||||||
 | 
					  dotenv (~> 3.1)
 | 
				
			||||||
  importmap-rails
 | 
					  importmap-rails
 | 
				
			||||||
  jbuilder
 | 
					  jbuilder
 | 
				
			||||||
  pg (~> 1.1)
 | 
					  pg (~> 1.1)
 | 
				
			||||||
  propshaft
 | 
					  propshaft
 | 
				
			||||||
  puma (>= 5.0)
 | 
					  puma (>= 5.0)
 | 
				
			||||||
  rails (~> 8.0.2)
 | 
					  rails (~> 8.0.2)
 | 
				
			||||||
 | 
					  rest-client (~> 2.1)
 | 
				
			||||||
  rubocop-rails-omakase
 | 
					  rubocop-rails-omakase
 | 
				
			||||||
  selenium-webdriver
 | 
					  selenium-webdriver
 | 
				
			||||||
  solid_queue
 | 
					  solid_queue
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,16 @@
 | 
				
			||||||
class ApplicationController < ActionController::Base
 | 
					class ApplicationController < ActionController::Base
 | 
				
			||||||
  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
 | 
					  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
 | 
				
			||||||
  allow_browser versions: :modern
 | 
					  allow_browser versions: :modern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action :check_spotify_login
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def check_spotify_login
 | 
				
			||||||
 | 
					    if user_signed_in? && current_user.logins.where(platform: "spotify").none?
 | 
				
			||||||
 | 
					      unless request.path == spotify_connect_path || request.path == spotify_path || request.path == spotify_callback_path || devise_controller?
 | 
				
			||||||
 | 
					        redirect_to spotify_path
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					class SpotifyController < ApplicationController
 | 
				
			||||||
 | 
					  before_action :authenticate_user!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def index
 | 
				
			||||||
 | 
					    # If already connected, redirect to dashboard
 | 
				
			||||||
 | 
					    redirect_to statistics_path if current_user.logins.exists?(platform: "spotify")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def connect
 | 
				
			||||||
 | 
					    # If user already has a login, redirect to statistics dashboard
 | 
				
			||||||
 | 
					    return redirect_to statistics_path if current_user.logins.exists?(platform: "spotify")
 | 
				
			||||||
 | 
					    client_id = ENV["SPOTIFY_CLIENT_ID"]
 | 
				
			||||||
 | 
					    redirect_uri = SpotifyClient::SPOTIFY_REDIRECT_URI
 | 
				
			||||||
 | 
					    scope = SpotifyClient::SCOPE
 | 
				
			||||||
 | 
					    state = SecureRandom.hex(16)
 | 
				
			||||||
 | 
					    session[:spotify_auth_state] = state
 | 
				
			||||||
 | 
					    auth_url = "https://accounts.spotify.com/authorize?" + {
 | 
				
			||||||
 | 
					      client_id: client_id,
 | 
				
			||||||
 | 
					      response_type: "code",
 | 
				
			||||||
 | 
					      redirect_uri: redirect_uri,
 | 
				
			||||||
 | 
					      scope: scope,
 | 
				
			||||||
 | 
					      state: state
 | 
				
			||||||
 | 
					    }.to_query
 | 
				
			||||||
 | 
					    redirect_to auth_url, allow_other_host: true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def callback
 | 
				
			||||||
 | 
					    if params[:state] != session.delete(:spotify_auth_state)
 | 
				
			||||||
 | 
					      return redirect_to spotify_path, alert: "Invalid state parameter. Please try again."
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    if params[:code].present?
 | 
				
			||||||
 | 
					      if handle_spotify_callback(params[:code])
 | 
				
			||||||
 | 
					        redirect_to statistics_path, notice: "Spotify authorization successful."
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        redirect_to spotify_path, alert: "Spotify authorization failed. Please try again."
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      redirect_to spotify_path, alert: "Spotify authorization failed."
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_spotify_callback(code)
 | 
				
			||||||
 | 
					    begin
 | 
				
			||||||
 | 
					      token_response = SpotifyClient.new(current_user).token_response_from_code(code)
 | 
				
			||||||
 | 
					      Login.find_or_create_for_response!(current_user, token_response)
 | 
				
			||||||
 | 
					      true
 | 
				
			||||||
 | 
					    rescue RestClient::Exception, JSON::ParserError => e
 | 
				
			||||||
 | 
					      Rails.logger.error("Spotify callback error: #{e.class} - #{e.message}")
 | 
				
			||||||
 | 
					      false
 | 
				
			||||||
 | 
					    rescue StandardError => e
 | 
				
			||||||
 | 
					      Rails.logger.error("Unexpected Spotify callback error: #{e.class} - #{e.message}")
 | 
				
			||||||
 | 
					      false
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					class LoadActivitiesJob < ApplicationJob
 | 
				
			||||||
 | 
					  def perform
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,37 @@
 | 
				
			||||||
class Login < ApplicationRecord
 | 
					class Login < ApplicationRecord
 | 
				
			||||||
  belongs_to :user
 | 
					  belongs_to :user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  scope :alive, -> { where(dead_since: nil) }
 | 
				
			||||||
 | 
					  scope :dead, -> { where.not(dead_since: nil) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CLOSE_TO_EXPIRATION = 60
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def self.find_or_create_for_response!(user, response, platform = "spotify")
 | 
				
			||||||
 | 
					    login = user.logins.find_by_platform(platform)
 | 
				
			||||||
 | 
					    login ||= Login.new(
 | 
				
			||||||
 | 
					      user: user,
 | 
				
			||||||
 | 
					      platform: platform,
 | 
				
			||||||
 | 
					      last_refresh_at: Time.new
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    login.credentials = response
 | 
				
			||||||
 | 
					    login.dead_since = nil
 | 
				
			||||||
 | 
					    login.save!
 | 
				
			||||||
 | 
					    login
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update_access_token!(spotify_response)
 | 
				
			||||||
 | 
					    self.credentials = credentials.merge(spotify_response)
 | 
				
			||||||
 | 
					    self.last_refresh_at = Time.new
 | 
				
			||||||
 | 
					    save!
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def about_to_expire?
 | 
				
			||||||
 | 
					    Time.new > last_refresh_at + credentials["expires_in"].to_i - CLOSE_TO_EXPIRATION
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def expired?
 | 
				
			||||||
 | 
					    Time.new > last_refresh_at + credentials["expires_in"].to_i
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,8 @@ class User < ApplicationRecord
 | 
				
			||||||
  devise :database_authenticatable, :registerable,
 | 
					  devise :database_authenticatable, :registerable,
 | 
				
			||||||
         :recoverable, :rememberable, :validatable
 | 
					         :recoverable, :rememberable, :validatable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_many :logins
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def is_admin?
 | 
					  def is_admin?
 | 
				
			||||||
    id == 1
 | 
					    id == 1
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +0,0 @@
 | 
				
			||||||
class NetflixShakti
 | 
					 | 
				
			||||||
  URL = "https://www.netflix.com/de/".freeze
 | 
					 | 
				
			||||||
  SHAKTI_PART = "BUILD_IDENTIFIER".freeze
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def self.call
 | 
					 | 
				
			||||||
    RestClient.get(URL)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def self.current_version
 | 
					 | 
				
			||||||
    body = call.body
 | 
					 | 
				
			||||||
    shakti = body.split(SHAKTI_PART)[1].split("\"")[2]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    shakti
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					class CreateSpotifyActivity
 | 
				
			||||||
 | 
					  attr_accessor :response, :user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def initialize(user, spotify_response)
 | 
				
			||||||
 | 
					    @response = spotify_response
 | 
				
			||||||
 | 
					    @user = user
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # rubocop:disable Metrics/AbcSize
 | 
				
			||||||
 | 
					  def perform
 | 
				
			||||||
 | 
					    response['items'].reverse.each do |play|
 | 
				
			||||||
 | 
					      activity = new_activity
 | 
				
			||||||
 | 
					      activity.item_ref = play['track']['id']
 | 
				
			||||||
 | 
					      artist_names = play['track']['artists'].map { |a| a['name'] }
 | 
				
			||||||
 | 
					      artists = artist_names.join(', ')
 | 
				
			||||||
 | 
					      title = "#{artists} - #{play['track']['name']}"
 | 
				
			||||||
 | 
					      activity.item_title = title
 | 
				
			||||||
 | 
					      activity.item_length = play['track']['duration'].to_s
 | 
				
			||||||
 | 
					      activity.started_at = DateTime.parse(play['played_at'])
 | 
				
			||||||
 | 
					      activity.save
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					  # rubocop:enable Metrics/AbcSize
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def new_activity
 | 
				
			||||||
 | 
					    Activity.new(user: user, platform: 'spotify')
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					class InitSpotifyLogin
 | 
				
			||||||
 | 
					  def self.create_new_login
 | 
				
			||||||
 | 
					    spotify = Spotify.new
 | 
				
			||||||
 | 
					    puts 'Enter email:'
 | 
				
			||||||
 | 
					    email = $stdin.gets.chomp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user = User.find_or_create_by!(email: email)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    puts "\nopen in browser:"
 | 
				
			||||||
 | 
					    puts spotify.auth_redirect_url.to_s
 | 
				
			||||||
 | 
					    puts "\ncopy url from browser and paste: "
 | 
				
			||||||
 | 
					    code_url = $stdin.gets.chomp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    puts 'parsing...'
 | 
				
			||||||
 | 
					    response = spotify.auth_code_authorize_url(code_url)
 | 
				
			||||||
 | 
					    Login.find_or_create_for_response!(user, response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    puts 'login created successful'
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					class LoadPlays
 | 
				
			||||||
 | 
					  attr_reader :user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def initialize(user)
 | 
				
			||||||
 | 
					    @user = user
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def perform
 | 
				
			||||||
 | 
					    user.logins.alive.each do |login|
 | 
				
			||||||
 | 
					      if login.spotify?
 | 
				
			||||||
 | 
					        update_spotify(login)
 | 
				
			||||||
 | 
					      elsif login.netflix?
 | 
				
			||||||
 | 
					        update_netflix(login)
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        puts "login #{login.id}, #{login.type} unknown"
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update_spotify(login)
 | 
				
			||||||
 | 
					    client = Spotify.new(login)
 | 
				
			||||||
 | 
					    last_spotify_activity_at = latest_activity_iso(login.platform)
 | 
				
			||||||
 | 
					    new_activities = client.load_since(last_spotify_activity_at)
 | 
				
			||||||
 | 
					    create_action = CreateSpotifyActivity.new(user, new_activities)
 | 
				
			||||||
 | 
					    create_action.perform
 | 
				
			||||||
 | 
					  rescue StandardError => e
 | 
				
			||||||
 | 
					    Rails.logger.error(e)
 | 
				
			||||||
 | 
					    LogLoadFailedWorker.perform_async(login.id)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update_netflix(login)
 | 
				
			||||||
 | 
					    client = Netflix.new(login)
 | 
				
			||||||
 | 
					    latest_netflix_activity = latest_activity(login.platform)
 | 
				
			||||||
 | 
					    new_activites = client.viewingactivity
 | 
				
			||||||
 | 
					    create_action = CreateNetflixActivity.new(user, latest_netflix_activity, new_activites)
 | 
				
			||||||
 | 
					    create_action.perform
 | 
				
			||||||
 | 
					  rescue StandardError => e
 | 
				
			||||||
 | 
					    Rails.logger.error(e)
 | 
				
			||||||
 | 
					    LogLoadFailedWorker.perform_async(login.id)
 | 
				
			||||||
 | 
					    # Rails.logger.info("trying to update shakti")
 | 
				
			||||||
 | 
					    # ReloadShaktiPath.new.perform
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def latest_activity(platform)
 | 
				
			||||||
 | 
					    latest = Activity.where(user: user, platform: platform).order(started_at: :desc).limit(1).last
 | 
				
			||||||
 | 
					    latest.started_at&.to_datetime if latest.present?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def latest_activity_iso(platform)
 | 
				
			||||||
 | 
					    latest = latest_activity(platform)
 | 
				
			||||||
 | 
					    latest.strftime('%Q') if latest.present?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					require "rest-client"
 | 
				
			||||||
 | 
					require "base64"
 | 
				
			||||||
 | 
					require "json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SpotifyClient
 | 
				
			||||||
 | 
					  SPOTIFY_REDIRECT_URI = "#{ENV['SPOTIFY_REDIRECT_URI']}/spotify_callback".freeze
 | 
				
			||||||
 | 
					  SCOPE = "user-read-recently-played".freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  BASE_URL = "https://api.spotify.com/v1/".freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr_reader :login
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def initialize(login = nil)
 | 
				
			||||||
 | 
					    @login = login
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def load_since(load_since)
 | 
				
			||||||
 | 
					    params = {
 | 
				
			||||||
 | 
					      "limit" => "50"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    params["after"] = load_since unless load_since.nil?
 | 
				
			||||||
 | 
					    get("me/player/recently-played", params)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def token_response_from_code(code)
 | 
				
			||||||
 | 
					    auth_code_authorize(code)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def auth_redirect_url
 | 
				
			||||||
 | 
					    "https://accounts.spotify.com/authorize?" \
 | 
				
			||||||
 | 
					    "response_type=code&" \
 | 
				
			||||||
 | 
					    "client_id=#{ENV['SPOTIFY_CLIENT_ID']}&" \
 | 
				
			||||||
 | 
					    "scope=#{SCOPE}&" \
 | 
				
			||||||
 | 
					    "redirect_uri=#{CGI.escape(SPOTIFY_REDIRECT_URI)}"
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def get(path, params = {})
 | 
				
			||||||
 | 
					    refresh_token!
 | 
				
			||||||
 | 
					    begin
 | 
				
			||||||
 | 
					      call(path, params)
 | 
				
			||||||
 | 
					    rescue RestClient::ExceptionWithResponse => e
 | 
				
			||||||
 | 
					      if e.response.code == 401
 | 
				
			||||||
 | 
					        puts "token expired, forcing refresh"
 | 
				
			||||||
 | 
					        refresh_token!(force: true)
 | 
				
			||||||
 | 
					        begin
 | 
				
			||||||
 | 
					          call(path, params)
 | 
				
			||||||
 | 
					        rescue RestClient::ExceptionWithResponse
 | 
				
			||||||
 | 
					          puts "token dead"
 | 
				
			||||||
 | 
					          LogLoadFailedWorker.perform_async(login.id)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def call(path, params)
 | 
				
			||||||
 | 
					    headers = { "Authorization" => "Bearer #{login.credentials['access_token']}" }
 | 
				
			||||||
 | 
					    headers[:params] = params if params.present?
 | 
				
			||||||
 | 
					    JSON.parse RestClient.get("#{BASE_URL}#{path}", headers)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def refresh_token!(force: false)
 | 
				
			||||||
 | 
					    return unless login.about_to_expire? || force
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    request_body = {
 | 
				
			||||||
 | 
					      'refresh_token': login.credentials["refresh_token"],
 | 
				
			||||||
 | 
					      'grant_type': "refresh_token"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    begin
 | 
				
			||||||
 | 
					      auth_response = RestClient.post(
 | 
				
			||||||
 | 
					        "https://accounts.spotify.com/api/token",
 | 
				
			||||||
 | 
					        request_body,
 | 
				
			||||||
 | 
					        client_auth_headers
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      new_credentials = JSON.parse auth_response
 | 
				
			||||||
 | 
					      login.update_access_token!(new_credentials)
 | 
				
			||||||
 | 
					    rescue RestClient::ExceptionWithResponse => e
 | 
				
			||||||
 | 
					      login.update(dead_since: Time.new)
 | 
				
			||||||
 | 
					      raise e
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def client_auth_headers
 | 
				
			||||||
 | 
					    auth_plain = "#{ENV['SPOTIFY_CLIENT_ID']}:#{ENV['SPOTIFY_CLIENT_SECRET']}"
 | 
				
			||||||
 | 
					    auth_string = Base64.strict_encode64 auth_plain
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      'Authorization': "Basic #{auth_string}"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def auth_code_authorize(code)
 | 
				
			||||||
 | 
					    request_body = {
 | 
				
			||||||
 | 
					      'code': code,
 | 
				
			||||||
 | 
					      'redirect_uri': SPOTIFY_REDIRECT_URI,
 | 
				
			||||||
 | 
					      'grant_type': "authorization_code"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    begin
 | 
				
			||||||
 | 
					      auth_response = RestClient.post(
 | 
				
			||||||
 | 
					        "https://accounts.spotify.com/api/token",
 | 
				
			||||||
 | 
					        request_body,
 | 
				
			||||||
 | 
					        client_auth_headers
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    rescue RestClient::ExceptionWithResponse => e
 | 
				
			||||||
 | 
					      raise e
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    raise StandardError.new("Authorization failed", auth_response) unless auth_response.code == 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    JSON.parse auth_response.body
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					<div class="dashboard-widget dashboard-spotify-connect">
 | 
				
			||||||
 | 
					  <h2 style="margin-top:0; font-size: 1.4em; color: #1DB954;">Connect to Spotify</h2>
 | 
				
			||||||
 | 
					  <p>To get started, connect your Spotify account. This will allow Plays Hub to load your listening activity and statistics.</p>
 | 
				
			||||||
 | 
					  <%= button_to "Connect with Spotify", spotify_connect_path, method: :post, class: "spotify-connect-btn" %>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					<div class="dashboard-widget dashboard-spotify-connect">
 | 
				
			||||||
 | 
					  <h2 style="margin-top:0; font-size: 1.4em; color: #1DB954;">Connect to Spotify</h2>
 | 
				
			||||||
 | 
					  <p>To use Plays Hub, you need to connect your Spotify account. This allows us to load your listening activity and generate your statistics dashboard.</p>
 | 
				
			||||||
 | 
					  <%= button_to "Connect with Spotify", spotify_connect_path, method: :get, class: "spotify-connect-btn", style: "margin-top: 1.5em; background: #1DB954; color: #fff; border: none; border-radius: 12px; padding: 0.8em 2.2em; font-size: 1.1em; font-weight: 600; cursor: pointer;" %>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -8,3 +8,10 @@
 | 
				
			||||||
#     command: "SoftDeletedRecord.due.delete_all"
 | 
					#     command: "SoftDeletedRecord.due.delete_all"
 | 
				
			||||||
#     priority: 2
 | 
					#     priority: 2
 | 
				
			||||||
#     schedule: at 5am every day
 | 
					#     schedule: at 5am every day
 | 
				
			||||||
 | 
					# development:
 | 
				
			||||||
 | 
					#   periodic_command:
 | 
				
			||||||
 | 
					#     class: TestJob
 | 
				
			||||||
 | 
					#     queue: background
 | 
				
			||||||
 | 
					#     schedule: every minute
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
| 
						 | 
					@ -13,4 +13,8 @@ Rails.application.routes.draw do
 | 
				
			||||||
  # Defines the root path route ("/")
 | 
					  # Defines the root path route ("/")
 | 
				
			||||||
  root "statistics#index"
 | 
					  root "statistics#index"
 | 
				
			||||||
  get "statistics", to: "statistics#index", as: :statistics
 | 
					  get "statistics", to: "statistics#index", as: :statistics
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get "spotify", to: "spotify#index", as: :spotify
 | 
				
			||||||
 | 
					  get "spotify/connect", to: "spotify#connect", as: :spotify_connect
 | 
				
			||||||
 | 
					  get "spotify_callback", to: "spotify#callback", as: :spotify_callback
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue