spotify web setup is done
							parent
							
								
									b368a750fe
								
							
						
					
					
						commit
						5eaad428ff
					
				
							
								
								
									
										4
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										4
									
								
								Gemfile
								
								
								
								
							| 
						 | 
				
			
			@ -54,3 +54,7 @@ group :test do
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
      responders
 | 
			
		||||
      warden (~> 1.2.3)
 | 
			
		||||
    domain_name (0.6.20240107)
 | 
			
		||||
    dotenv (3.1.8)
 | 
			
		||||
    drb (2.2.3)
 | 
			
		||||
    erb (5.0.1)
 | 
			
		||||
    erubi (1.13.1)
 | 
			
		||||
| 
						 | 
				
			
			@ -117,6 +119,9 @@ GEM
 | 
			
		|||
      raabro (~> 1.4)
 | 
			
		||||
    globalid (1.2.1)
 | 
			
		||||
      activesupport (>= 6.1)
 | 
			
		||||
    http-accept (1.7.0)
 | 
			
		||||
    http-cookie (1.0.8)
 | 
			
		||||
      domain_name (~> 0.5)
 | 
			
		||||
    i18n (1.14.7)
 | 
			
		||||
      concurrent-ruby (~> 1.0)
 | 
			
		||||
    importmap-rails (2.1.0)
 | 
			
		||||
| 
						 | 
				
			
			@ -145,6 +150,10 @@ GEM
 | 
			
		|||
      net-smtp
 | 
			
		||||
    marcel (1.0.4)
 | 
			
		||||
    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)
 | 
			
		||||
    minitest (5.25.5)
 | 
			
		||||
    msgpack (1.8.0)
 | 
			
		||||
| 
						 | 
				
			
			@ -157,6 +166,7 @@ GEM
 | 
			
		|||
      timeout
 | 
			
		||||
    net-smtp (0.5.1)
 | 
			
		||||
      net-protocol
 | 
			
		||||
    netrc (0.11.0)
 | 
			
		||||
    nio4r (2.7.4)
 | 
			
		||||
    nokogiri (1.18.8-aarch64-linux-gnu)
 | 
			
		||||
      racc (~> 1.4)
 | 
			
		||||
| 
						 | 
				
			
			@ -245,6 +255,11 @@ GEM
 | 
			
		|||
    responders (3.1.1)
 | 
			
		||||
      actionpack (>= 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)
 | 
			
		||||
    rubocop (1.77.0)
 | 
			
		||||
      json (~> 2.3)
 | 
			
		||||
| 
						 | 
				
			
			@ -339,12 +354,14 @@ DEPENDENCIES
 | 
			
		|||
  capybara
 | 
			
		||||
  debug
 | 
			
		||||
  devise (~> 4.9)
 | 
			
		||||
  dotenv (~> 3.1)
 | 
			
		||||
  importmap-rails
 | 
			
		||||
  jbuilder
 | 
			
		||||
  pg (~> 1.1)
 | 
			
		||||
  propshaft
 | 
			
		||||
  puma (>= 5.0)
 | 
			
		||||
  rails (~> 8.0.2)
 | 
			
		||||
  rest-client (~> 2.1)
 | 
			
		||||
  rubocop-rails-omakase
 | 
			
		||||
  selenium-webdriver
 | 
			
		||||
  solid_queue
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,16 @@
 | 
			
		|||
class ApplicationController < ActionController::Base
 | 
			
		||||
  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
 | 
			
		||||
  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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
  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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,8 @@ class User < ApplicationRecord
 | 
			
		|||
  devise :database_authenticatable, :registerable,
 | 
			
		||||
         :recoverable, :rememberable, :validatable
 | 
			
		||||
 | 
			
		||||
  has_many :logins
 | 
			
		||||
 | 
			
		||||
  def is_admin?
 | 
			
		||||
    id == 1
 | 
			
		||||
  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"
 | 
			
		||||
#     priority: 2
 | 
			
		||||
#     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 ("/")
 | 
			
		||||
  root "statistics#index"
 | 
			
		||||
  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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue