Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions MyMusicBoxApi/converter/create_playlist_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import requests
import json
import time

# === CONFIGURATION ===
SPOTIFY_ACCESS_TOKEN = ""
YOUTUBE_ACCESS_TOKEN = ""
SPOTIFY_PLAYLIST_ID = "" # Only the ID, not full URL
MAX_SONGS = 100 # Adjust as needed

SPOTIFY_API_BASE = "https://api.spotify.com/v1"
YOUTUBE_API_BASE = "https://www.googleapis.com/youtube/v3"

# === STEP 1: Get Songs from a Spotify Playlist ===
def get_spotify_playlist_songs(playlist_id, max_songs=None):
headers = {"Authorization": f"Bearer {SPOTIFY_ACCESS_TOKEN}"}
url = f"{SPOTIFY_API_BASE}/playlists/{playlist_id}/tracks?limit=100"
songs = []

while url:
response = requests.get(url, headers=headers)
if response.status_code != 200:
print(f"Failed to fetch playlist songs: {response.status_code} — {response.text}")
break

data = response.json()
for item in data.get('items', []):
track = item.get('track')
if track:
name = track['name']
artists = ', '.join(artist['name'] for artist in track['artists'])
query = f"{artists} {name}"
songs.append(query)

# Optional cap
if max_songs and len(songs) >= max_songs:
return songs

url = data.get('next') # Spotify handles pagination via `next`
time.sleep(0.1) # Throttle to be API-friendly

return songs

# === STEP 2: Search YouTube Video ===
def search_youtube_video(query):
headers = {"Authorization": f"Bearer {YOUTUBE_ACCESS_TOKEN}"}
params = {
'part': 'snippet',
'q': query,
'maxResults': 1,
'type': 'video'
}

response = requests.get(f"{YOUTUBE_API_BASE}/search", headers=headers, params=params)
if response.status_code != 200:
print(f"Failed YouTube search: {response.status_code} — {query}")
return None

items = response.json().get('items', [])
if items:
return items[0]['id']['videoId']
return None

# === STEP 3: Create YouTube Playlist ===
def create_youtube_playlist(title, description="Imported from Spotify Playlist"):
headers = {
"Authorization": f"Bearer {YOUTUBE_ACCESS_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"snippet": {
"title": title,
"description": description
},
"status": {
"privacyStatus": "private"
}
}

response = requests.post(
f"{YOUTUBE_API_BASE}/playlists?part=snippet,status",
headers=headers,
data=json.dumps(payload)
)

if response.status_code != 200:
print(f"Failed to create playlist: {response.status_code} — {response.text}")
return None

return response.json().get("id")

# === STEP 4: Add Video to YouTube Playlist ===
def add_video_to_playlist(video_id, playlist_id):
headers = {
"Authorization": f"Bearer {YOUTUBE_ACCESS_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"snippet": {
"playlistId": playlist_id,
"resourceId": {
"kind": "youtube#video",
"videoId": video_id
}
}
}

response = requests.post(
f"{YOUTUBE_API_BASE}/playlistItems?part=snippet",
headers=headers,
data=json.dumps(payload)
)

if response.status_code not in (200, 201):
print(f"Failed to add video {video_id} to playlist: {response.status_code}")
return False

return True

# === MAIN EXECUTION ===
def main():
print("▶ Fetching songs from Spotify playlist...")
songs = get_spotify_playlist_songs(SPOTIFY_PLAYLIST_ID)
print(f"🎵 Found {len(songs)} songs in playlist.")
with open("playlist.txt", "a") as f:
for line in songs:
f.write(f"{line}\n")
# print("📺 Creating YouTube playlist...")
# playlist_title = "Spotify Playlist to YouTube"
# playlist_id = create_youtube_playlist(playlist_title)
# if not playlist_id:
# print("❌ Could not create YouTube playlist.")
# return

print("🔁 Searching on YouTube and adding to playlist...")
for idx, song in enumerate(songs, 1):
print(f"[{idx}/{len(songs)}] Searching: {song}")
# video_id = search_youtube_video(song)
# if video_id:
# if add_video_to_playlist(video_id, playlist_id):
# print(" ✅ Added successfully")
# else:
# print(" ❌ Failed to add")
# else:
# print(" ❌ No video found")
# time.sleep(1.1) # Be gentle with YouTube's API

print("✅ Done!")

if __name__ == "__main__":
main()

6 changes: 3 additions & 3 deletions MyMusicBoxApi/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const ReturningIdParameter = "RETURNING"
const ReturningIdParameterLower = "returning"
const DatabaseDriver = "postgres"
const MigrationFolder = "migration_scripts"
const MaxOpenConnections = 10
const MaxIdleConnections = 5
const MaxOpenConnections = 15
const MaxIdleConnections = 10
const MaxConnectionIdleTimeInMinutes = 10
const MaxConnectionLifeTimeInMinutes = 10

Expand Down Expand Up @@ -164,7 +164,7 @@ func (base *BaseTable) QueryRows(query string) (*sql.Rows, error) {
}

func ApplyMigrations() {
logging.Info("Applying migrations...")
logging.Info("Checking for database migration files")
// files will be sorted by filename
// to make sure the migrations are executed in order
// this naming convention must be used
Expand Down
17 changes: 16 additions & 1 deletion MyMusicBoxApi/database/migrationtable.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,22 @@ func (table *MigrationTable) Insert(filename string, contents string) (err error
}

func (table *MigrationTable) ApplyMigration(query string) (err error) {
return table.NonScalarQuery(query)

transaction, err := table.DB.Begin()

if err != nil {
logging.Error(fmt.Sprintf("Failed to creare transaction %s", err.Error()))
return
}

_, err = transaction.Exec(query)

if err != nil {
logging.Error(fmt.Sprintf("Failed to execute query %s", err.Error()))
return
}

return transaction.Commit()
}

func (table *MigrationTable) GetCurrentAppliedMigrationFileName() (fileName string, err error) {
Expand Down
1 change: 0 additions & 1 deletion MyMusicBoxApi/database/migrationtable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ func TestApplyMigration(t *testing.T) {
query := "DROP DATABSE migration"

mock.ExpectBegin()
mock.ExpectPrepare(query)
mock.ExpectExec(query).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
Expand Down
127 changes: 88 additions & 39 deletions MyMusicBoxApi/database/tasklogtable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ package database

import (
"context"
"fmt"
"musicboxapi/logging"
"musicboxapi/models"
"time"
)

type ITasklogTable interface {
InsertTaskLog() (lastInsertedId int, err error)
UpdateTaskLogStatus(taskId int, nStatus int) (err error)
EndTaskLog(taskId int, nStatus int, data []byte) (err error)
UpdateTaskLogError(params ...any) (err error)
GetTaskLogs(ctx context.Context) ([]models.TaskLog, error)
GetParentLogs(ctx context.Context) ([]models.ParentTaskLog, error)
GetChildLogs(ctx context.Context, parentId int) ([]models.ChildTaskLog, error)
CreateParentTaskLog(url string) (models.ParentTaskLog, error)
CreateChildTaskLog(parent models.ParentTaskLog) (models.ChildTaskLog, error)
UpdateChildTaskLogStatus(child models.ChildTaskLog) error
ChildTaskLogDone(child models.ChildTaskLog) error
ChildTaskLogError(child models.ChildTaskLog) error
}

type TasklogTable struct {
Expand All @@ -25,59 +25,108 @@ func NewTasklogTableInstance() *TasklogTable {
BaseTable: NewBaseTableInstance(),
}
}
func (table *TasklogTable) GetParentLogs(ctx context.Context) ([]models.ParentTaskLog, error) {
query := "SELECT * FROM ParentTaskLog ORDER BY AddTime desc"

func (table *TasklogTable) InsertTaskLog() (lastInsertedId int, error error) {
query := `INSERT INTO TaskLog (Status) VALUES($1) RETURNING Id`
rows, err := table.QueryRowsContex(ctx, query)

lastInsertedId, err := table.InsertWithReturningId(query, int(models.Pending))
if err != nil {
return make([]models.ParentTaskLog, 0), nil
}

return lastInsertedId, err
}
defer rows.Close()

func (table *TasklogTable) UpdateTaskLogStatus(taskId int, nStatus int) (error error) {
query := `UPDATE TaskLog SET Status = $1 WHERE Id = $2`
var parentLog models.ParentTaskLog
logs := make([]models.ParentTaskLog, 0)

return table.NonScalarQuery(query, nStatus, taskId)
}
for rows.Next() {
err := rows.Scan(&parentLog.Id, &parentLog.Url, &parentLog.AddTime)

func (table *TasklogTable) EndTaskLog(taskId int, nStatus int, data []byte) error {
query := `UPDATE TaskLog SET Status = $1, OutputLog = $2, EndTime = $3 WHERE Id = $4`
if err != nil {
logging.ErrorStackTrace(err)
continue
}

return table.NonScalarQuery(query, nStatus, data, time.Now(), taskId)
}
logs = append(logs, parentLog)
}

func (table *TasklogTable) UpdateTaskLogError(params ...any) error {
query := `UPDATE TaskLog
SET Status = $1, OutputLog = $2, EndTime = $3
WHERE Id = $4`
return table.NonScalarQuery(query, params...)
return logs, nil
}
func (table *TasklogTable) GetChildLogs(ctx context.Context, parentId int) ([]models.ChildTaskLog, error) {
query := "SELECT * FROM ChildTaskLog WHERE ParentId = $1 ORDER BY StartTime desc"

func (table *TasklogTable) GetTaskLogs(ctx context.Context) ([]models.TaskLog, error) {
query := `SELECT Id, StartTime, EndTime, Status, OutputLog FROM TaskLog ORDER BY Id desc` // get the latest first

rows, err := table.QueryRowsContex(ctx, query)
rows, err := table.QueryRowsContex(ctx, query, parentId)

if err != nil {
logging.Error(fmt.Sprintf("QueryRow error: %s", err.Error()))
return nil, err
return make([]models.ChildTaskLog, 0), nil
}
defer rows.Close()

var tasklog models.TaskLog
defer rows.Close()

tasks := make([]models.TaskLog, 0)
var childLog models.ChildTaskLog
logs := make([]models.ChildTaskLog, 0)

for rows.Next() {
scanError := rows.Scan(&tasklog.Id, &tasklog.StartTime, &tasklog.EndTime, &tasklog.Status, &tasklog.OutputLog)
err := rows.Scan(&childLog.Id, &childLog.ParentId, &childLog.StartTime, &childLog.EndTime, &childLog.Status, &childLog.OutputLog)

if scanError != nil {
logging.Error(fmt.Sprintf("Scan error: %s", scanError.Error()))
if err != nil {
logging.ErrorStackTrace(err)
continue
}

tasks = append(tasks, tasklog)
logs = append(logs, childLog)
}

return logs, nil
}
func (table *TasklogTable) CreateParentTaskLog(url string) (models.ParentTaskLog, error) {
query := "INSERT INTO ParentTaskLog (Url) Values($1) RETURNING Id"

id, err := table.InsertWithReturningId(query, url)

if err != nil {
return models.ParentTaskLog{}, err
}

return models.ParentTaskLog{
Id: id,
Url: url,
}, nil
}
func (table *TasklogTable) CreateChildTaskLog(parent models.ParentTaskLog) (models.ChildTaskLog, error) {
query := "INSERT INTO ChildTaskLog (ParentId, Status) VALUES($1,$2) RETURNING Id"

defaultStatus := int(models.Pending)

id, err := table.InsertWithReturningId(query, parent.Id, defaultStatus)

if err != nil {
return models.ChildTaskLog{}, err
}

return tasks, nil
return models.ChildTaskLog{
Id: id,
ParentId: parent.Id,
Status: defaultStatus,
}, nil
}
func (table *TasklogTable) UpdateChildTaskLogStatus(child models.ChildTaskLog) error {

if child.Status == int(models.Downloading) {
// set the start time to now
query := "UPDATE ChildTaskLog SET StartTime = CURRENT_TIMESTAMP, Status = $1 WHERE Id = $2"
return table.NonScalarQuery(query, child.Status, child.Id)
} else {
// just update
query := "UPDATE ChildTaskLog SET Status = $1 WHERE Id = $2"
return table.NonScalarQuery(query, child.Status, child.Id)
}
}
func (table *TasklogTable) ChildTaskLogDone(child models.ChildTaskLog) error {
query := "UPDATE ChildTaskLog SET Status = $1, OutputLog = $2, EndTime = CURRENT_TIMESTAMP WHERE Id = $3"
return table.NonScalarQuery(query, int(models.Done), child.OutputLog, child.Id)
}
func (table *TasklogTable) ChildTaskLogError(child models.ChildTaskLog) error {
query := "UPDATE ChildTaskLog SET Status = $1, OutputLog = $2, EndTime = CURRENT_TIMESTAMP WHERE Id = $3"
return table.NonScalarQuery(query, int(models.Error), child.OutputLog, child.Id)
}
9 changes: 4 additions & 5 deletions MyMusicBoxApi/http/download.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package http

import (
"musicboxapi/database"
"musicboxapi/models"
"musicboxapi/service"
"net/http"
Expand All @@ -25,15 +24,15 @@ func DownloadRequest(ctx *gin.Context) {
return
}

tasklogTable := database.NewTasklogTableInstance()
//tasklogTable := database.NewTasklogTableInstance()
// Insert a new task
taskId, err := tasklogTable.InsertTaskLog()
// parentTask, err := tasklogTable.CreateParentTaskLog(request.Url)

if err != nil {
ctx.JSON(http.StatusInternalServerError, models.ErrorResponse(err))
return
}

go service.StartDownloadTask(taskId, request)
ctx.JSON(http.StatusOK, models.OkResponse(gin.H{"taskId": taskId}, "Started task"))
go service.StartDownloadTask(request)
ctx.JSON(http.StatusOK, models.OkResponse(gin.H{"": ""}, "Created"))
}
Loading