spotify web setup is done
This commit is contained in:
@@ -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
|
||||
|
||||
57
app/controllers/spotify_controller.rb
Normal file
57
app/controllers/spotify_controller.rb
Normal file
@@ -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
|
||||
4
app/jobs/load_activities_job.rb
Normal file
4
app/jobs/load_activities_job.rb
Normal file
@@ -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
|
||||
28
app/services/create_spotify_activity.rb
Normal file
28
app/services/create_spotify_activity.rb
Normal file
@@ -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
|
||||
20
app/services/init_spotify_login.rb
Normal file
20
app/services/init_spotify_login.rb
Normal file
@@ -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
|
||||
53
app/services/load_plays.rb
Normal file
53
app/services/load_plays.rb
Normal file
@@ -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
|
||||
113
app/services/spotify_client.rb
Normal file
113
app/services/spotify_client.rb
Normal file
@@ -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
|
||||
5
app/views/spotify/connect.html.erb
Normal file
5
app/views/spotify/connect.html.erb
Normal file
@@ -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>
|
||||
5
app/views/spotify/index.html.erb
Normal file
5
app/views/spotify/index.html.erb
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user