This commit is contained in:
Chris McCord 2022-01-27 13:03:42 -05:00
parent bfb0c07330
commit 214ec50f0e
8 changed files with 57 additions and 22 deletions

View file

@ -4,6 +4,7 @@ import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
let nowSeconds = () => Math.round(Date.now() / 1000) let nowSeconds = () => Math.round(Date.now() / 1000)
let rand = (min, max) => Math.floor(Math.random() * (max - min) + min)
let execJS = (selector, attr) => { let execJS = (selector, attr) => {
document.querySelectorAll(selector).forEach(el => liveSocket.execJS(el, el.getAttribute(attr))) document.querySelectorAll(selector).forEach(el => liveSocket.execJS(el, el.getAttribute(attr)))
@ -96,9 +97,13 @@ Hooks.AudioPlayer = {
mounted(){ mounted(){
this.playbackBeganAt = null this.playbackBeganAt = null
this.player = this.el.querySelector("audio") this.player = this.el.querySelector("audio")
this.player.addEventListener("ended", () => console.log("player: ended"))
this.player.addEventListener("stalled", () => console.log("player: stalled"))
this.player.addEventListener("suspend", () => console.log("player: suspend"))
this.player.addEventListener("waiting", () => console.log("player: waiting"))
this.playerDuration = 0 this.playerDuration = 0
this.currentTime = this.el.querySelector("#player-time") this.currentTime = this.el.querySelector("#player-time")
this.duration = this.el.querySelector("#player-duration") this.durationText = this.el.querySelector("#player-duration")
this.progress = this.el.querySelector("#player-progress") this.progress = this.el.querySelector("#player-progress")
let enableAudio = () => { let enableAudio = () => {
if(this.player.src){ if(this.player.src){
@ -123,7 +128,7 @@ Hooks.AudioPlayer = {
if(currentSrc === url && this.player.paused){ if(currentSrc === url && this.player.paused){
this.play({sync: true}) this.play({sync: true})
} else if(currentSrc !== url) { } else if(currentSrc !== url) {
this.player.src = `${url}?token=${token}` this.player.src = `${url}?token=${token}&proxy`
this.play({sync: true}) this.play({sync: true})
} }
}) })
@ -132,9 +137,15 @@ Hooks.AudioPlayer = {
}, },
play(opts = {}){ play(opts = {}){
console.log("play")
let {sync} = opts let {sync} = opts
clearInterval(this.progressTimer)
clearTimeout(this.nextTimer)
this.player.play().then(() => { this.player.play().then(() => {
if(sync){ this.player.currentTime = nowSeconds() - this.playbackBeganAt } if(sync){
console.log("sync", nowSeconds() - this.playbackBeganAt)
this.player.currentTime = nowSeconds() - this.playbackBeganAt
}
this.progressTimer = setInterval(() => this.updateProgress(), 100) this.progressTimer = setInterval(() => this.updateProgress(), 100)
}, error => { }, error => {
if(error.name === "NotAllowedError"){ if(error.name === "NotAllowedError"){
@ -145,28 +156,32 @@ Hooks.AudioPlayer = {
pause(){ pause(){
clearInterval(this.progressTimer) clearInterval(this.progressTimer)
clearTimeout(this.nextTimer)
this.player.pause() this.player.pause()
}, },
stop(){ stop(){
clearInterval(this.progressTimer) clearInterval(this.progressTimer)
clearTimeout(this.nextTimer)
this.player.pause() this.player.pause()
this.player.currentTime = 0 this.player.currentTime = 0
this.updateProgress() this.updateProgress()
this.duration.innerText = "" this.durationText.innerText = ""
this.currentTime.innerText = "" this.currentTime.innerText = ""
}, },
updateProgress(){ updateProgress(){
if(this.playerDuration === 0){ return false } if(this.playerDuration === 0){ return false }
if(Math.ceil(this.player.currentTime) >= Math.floor(this.playerDuration)){ if(Math.ceil(this.player.currentTime) >= Math.floor(this.playerDuration)){
this.playerDuration = 0
this.pushEvent("next_song_auto")
clearInterval(this.progressTimer) clearInterval(this.progressTimer)
this.player.pause()
this.playerDuration = 0
console.log("next_song_auto")
this.nextTimer = setTimeout(() => this.pushEvent("next_song_auto"), rand(1000, 3000))
return return
} }
this.progress.style.width = `${(this.player.currentTime / (this.playerDuration) * 100)}%` this.progress.style.width = `${(this.player.currentTime / (this.playerDuration) * 100)}%`
this.duration.innerText = this.formatTime(this.playerDuration) this.durationText.innerText = this.formatTime(this.playerDuration)
this.currentTime.innerText = this.formatTime(this.player.currentTime) this.currentTime.innerText = this.formatTime(this.player.currentTime)
}, },

View file

@ -3,7 +3,9 @@ import Config
config :live_beats, :files, config :live_beats, :files,
uploads_dir: Path.expand("../priv/uploads", __DIR__), uploads_dir: Path.expand("../priv/uploads", __DIR__),
host: [scheme: "http", host: "localhost", port: 4000], host: [scheme: "http", host: "localhost", port: 4000],
server_ip: "127.0.0.1" server_ip: "127.0.0.1",
hostname: "localhost",
transport_opts: []
config :live_beats, :github, config :live_beats, :github,
client_id: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_ID"), client_id: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_ID"),

View file

@ -64,7 +64,6 @@ if config_env() == :prod do
client_secret: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_SECRET") client_secret: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_SECRET")
config :libcluster, config :libcluster,
debug: true,
topologies: [ topologies: [
fly6pn: [ fly6pn: [
strategy: Cluster.Strategy.DNSPoll, strategy: Cluster.Strategy.DNSPoll,

View file

@ -138,7 +138,7 @@ defmodule LiveBeats.MediaLibrary do
end end
def put_stats(%Ecto.Changeset{} = changeset, %MP3Stat{} = stat) do def put_stats(%Ecto.Changeset{} = changeset, %MP3Stat{} = stat) do
chset = Song.put_duration(changeset, stat.duration) chset = Song.put_stats(changeset, stat)
if error = chset.errors[:duration] do if error = chset.errors[:duration] do
{:error, %{duration: error}} {:error, %{duration: error}}
@ -423,10 +423,11 @@ defmodule LiveBeats.MediaLibrary do
Song.changeset(song, attrs) Song.changeset(song, attrs)
end end
@keep_changes [:duration, :mp3_filesize, :mp3_filepath]
def change_song(%Ecto.Changeset{} = prev_changeset, attrs) do def change_song(%Ecto.Changeset{} = prev_changeset, attrs) do
%Song{} %Song{}
|> change_song(attrs) |> change_song(attrs)
|> Ecto.Changeset.change(Map.take(prev_changeset.changes, [:duration])) |> Ecto.Changeset.change(Map.take(prev_changeset.changes, @keep_changes))
end end
defp order_by_playlist(%Ecto.Query{} = query, direction) when direction in [:asc, :desc] do defp order_by_playlist(%Ecto.Query{} = query, direction) when direction in [:asc, :desc] do

View file

@ -19,6 +19,7 @@ defmodule LiveBeats.MediaLibrary.Song do
field :mp3_url, :string field :mp3_url, :string
field :mp3_filepath, :string field :mp3_filepath, :string
field :mp3_filename, :string field :mp3_filename, :string
field :mp3_filesize, :integer, default: 0
field :server_ip, EctoNetwork.INET field :server_ip, EctoNetwork.INET
belongs_to :user, Accounts.User belongs_to :user, Accounts.User
belongs_to :genre, LiveBeats.MediaLibrary.Genre belongs_to :genre, LiveBeats.MediaLibrary.Genre
@ -45,7 +46,13 @@ defmodule LiveBeats.MediaLibrary.Song do
put_assoc(changeset, :user, user) put_assoc(changeset, :user, user)
end end
def put_duration(%Ecto.Changeset{} = changeset, duration) when is_integer(duration) do def put_stats(%Ecto.Changeset{} = changeset, %LiveBeats.MP3Stat{} = stat) do
changeset
|> put_duration(stat.duration)
|> Ecto.Changeset.put_change(:mp3_filesize, stat.size)
end
defp put_duration(%Ecto.Changeset{} = changeset, duration) when is_integer(duration) do
changeset changeset
|> Ecto.Changeset.change(%{duration: duration}) |> Ecto.Changeset.change(%{duration: duration})
|> Ecto.Changeset.validate_number(:duration, |> Ecto.Changeset.validate_number(:duration,

View file

@ -8,7 +8,7 @@ defmodule LiveBeats.MP3Stat do
use Bitwise use Bitwise
alias LiveBeats.MP3Stat alias LiveBeats.MP3Stat
defstruct duration: 0, path: nil, title: nil, artist: nil, tags: nil defstruct duration: 0, size: 0, path: nil, title: nil, artist: nil, tags: nil
@declared_frame_ids ~w(AENC APIC ASPI COMM COMR ENCR EQU2 ETCO GEOB GRID LINK MCDI MLLT OWNE PRIV PCNT POPM POSS RBUF RVA2 RVRB SEEK SIGN SYLT SYTC TALB TBPM TCOM TCON TCOP TDEN TDLY TDOR TDRC TDRL TDTG TENC TEXT TFLT TIPL TIT1 TIT2 TIT3 TKEY TLAN TLEN TMCL TMED TMOO TOAL TOFN TOLY TOPE TOWN TPE1 TPE2 TPE3 TPE4 TPOS TPRO TPUB TRCK TRSN TRSO TSOA TSOP TSOT TSRC TSSE TSST TXXX UFID USER USLT WCOM WCOP WOAF WOAR WOAS WORS WPAY WPUB WXXX) @declared_frame_ids ~w(AENC APIC ASPI COMM COMR ENCR EQU2 ETCO GEOB GRID LINK MCDI MLLT OWNE PRIV PCNT POPM POSS RBUF RVA2 RVRB SEEK SIGN SYLT SYTC TALB TBPM TCOM TCON TCOP TDEN TDLY TDOR TDRC TDRL TDTG TENC TEXT TFLT TIPL TIT1 TIT2 TIT3 TKEY TLAN TLEN TMCL TMED TMOO TOAL TOFN TOLY TOPE TOWN TPE1 TPE2 TPE3 TPE4 TPOS TPRO TPUB TRCK TRSN TRSO TSOA TSOP TSOT TSRC TSSE TSST TXXX UFID USER USLT WCOM WCOP WOAF WOAR WOAS WORS WPAY WPUB WXXX)
@ -34,6 +34,7 @@ defmodule LiveBeats.MP3Stat do
end end
def parse(path) do def parse(path) do
stat = File.stat!(path)
{tag_info, rest} = parse_tag(File.read!(path)) {tag_info, rest} = parse_tag(File.read!(path))
duration = parse_frame(rest, 0, 0, 0) duration = parse_frame(rest, 0, 0, 0)
@ -44,7 +45,14 @@ defmodule LiveBeats.MP3Stat do
seconds = round(duration) seconds = round(duration)
{:ok, {:ok,
%MP3Stat{duration: seconds, path: path, tags: tag_info, title: title, artist: artist}} %MP3Stat{
duration: seconds,
size: stat.size,
path: path,
tags: tag_info,
title: title,
artist: artist
}}
_other -> _other ->
{:error, :bad_file} {:error, :bad_file}

View file

@ -8,18 +8,19 @@ defmodule LiveBeatsWeb.FileController do
require Logger require Logger
def show(conn, %{"id" => filename_uuid, "token" => token}) do def show(conn, %{"id" => filename_uuid, "token" => token} = params) do
path = MediaLibrary.local_filepath(filename_uuid) path = MediaLibrary.local_filepath(filename_uuid)
mime_type = MIME.from_path(path) mime_type = MIME.from_path(path)
case Phoenix.Token.decrypt(conn, "file", token, max_age: :timer.minutes(1)) do case Phoenix.Token.decrypt(conn, "file", token, max_age: :timer.minutes(1)) do
{:ok, %{uuid: ^filename_uuid, ip: ip}} -> {:ok, %{vsn: 1, uuid: ^filename_uuid, ip: ip, size: size}} ->
if local_file?(filename_uuid, ip) do # if local_file?(filename_uuid, ip) do
if !params["proxy"] do
Logger.info("serving file from #{server_ip()}") Logger.info("serving file from #{server_ip()}")
do_send_file(conn, path) do_send_file(conn, path)
else else
Logger.info("proxying file to #{ip} from #{server_ip()}") Logger.info("proxying file to #{ip} from #{server_ip()}")
proxy_file(conn, ip, mime_type) proxy_file(conn, ip, mime_type, size)
end end
{:ok, _} -> {:ok, _} ->
@ -38,10 +39,11 @@ defmodule LiveBeatsWeb.FileController do
|> send_file(200, path) |> send_file(200, path)
end end
defp proxy_file(conn, ip, mime_type) do defp proxy_file(conn, ip, mime_type, content_length) do
uri = conn |> request_url() |> URI.parse() uri = conn |> request_url() |> URI.parse()
port = LiveBeatsWeb.Endpoint.config(:http)[:port] port = LiveBeatsWeb.Endpoint.config(:http)[:port]
path = uri.path <> "?" <> uri.query <> "&from=#{server_ip()}" # path = uri.path <> "?" <> uri.query <> "&from=#{server_ip()}"
path = uri.path <> "?" <> String.replace(uri.query, "&proxy", "") <> "&from=#{server_ip()}"
{:ok, ipv6} = :inet.parse_address(String.to_charlist(ip)) {:ok, ipv6} = :inet.parse_address(String.to_charlist(ip))
{:ok, req} = Mint.HTTP.connect(:http, ipv6, port, file_server_opts()) {:ok, req} = Mint.HTTP.connect(:http, ipv6, port, file_server_opts())
{:ok, req, request_ref} = Mint.HTTP.request(req, "GET", path, [], "") {:ok, req, request_ref} = Mint.HTTP.request(req, "GET", path, [], "")
@ -49,7 +51,7 @@ defmodule LiveBeatsWeb.FileController do
conn conn
|> put_resp_header("content-type", mime_type) |> put_resp_header("content-type", mime_type)
|> put_resp_header("accept-ranges", "bytes") |> put_resp_header("accept-ranges", "bytes")
|> put_resp_header("transfer-encoding", "chunked") |> put_resp_header("content-length", IO.inspect(to_string(content_length)))
|> send_chunked(200) |> send_chunked(200)
|> stream(req, request_ref) |> stream(req, request_ref)
end end

View file

@ -301,7 +301,8 @@ defmodule LiveBeatsWeb.PlayerLive do
Phoenix.Token.encrypt(socket.endpoint, "file", %{ Phoenix.Token.encrypt(socket.endpoint, "file", %{
vsn: 1, vsn: 1,
ip: to_string(song.server_ip), ip: to_string(song.server_ip),
uuid: song.mp3_filename size: song.mp3_filesize,
uuid: song.mp3_filename,
}) })
push_event(socket, "play", %{ push_event(socket, "play", %{