Compare commits
12 Commits
81d29e4d8b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
964512de06 | ||
|
|
87e276de3d | ||
|
|
04e8378f3f | ||
|
|
293de8b473 | ||
|
|
82c541653c | ||
|
|
1ff37517a5 | ||
|
|
c7fceb277d | ||
|
|
8896cb7d09 | ||
|
|
c2794367b3 | ||
|
|
16318dac20 | ||
|
|
655087fa42 | ||
|
|
19185f38a3 |
@@ -50,6 +50,9 @@ func printResults(musicObjects []data.MusicObject) {
|
||||
if len(sources) > 0 {
|
||||
for _, source := range sources {
|
||||
results[i] += "\n\t- " + source.SourceType.Name + " " + string(source.ObjectType) + " " + source.Url
|
||||
if source.AudioUrl != "" {
|
||||
results[i] += "\n\t " + source.AudioUrl
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results[i] = color.StrikeThrough + results[i] + color.Reset
|
||||
@@ -73,21 +76,29 @@ func (s musicObjectStore) currentMusicObjects() ([]data.MusicObject, error) {
|
||||
|
||||
var indexSelectionPattern = regexp.MustCompile(`^[\d ,]+$`)
|
||||
|
||||
func interpretCommand(command string, store musicObjectStore) (musicObjectStore, error) {
|
||||
func interpretCommand(command string, store musicObjectStore) (musicObjectStore, []error) {
|
||||
// going back in history
|
||||
if command == ".." {
|
||||
if len(store) <= 1 {
|
||||
return store, errors.New("can't go back")
|
||||
return store, []error{errors.New("can't go back")}
|
||||
}
|
||||
|
||||
return store[:len(store)-1], nil
|
||||
return store[:len(store)-1], []error{}
|
||||
}
|
||||
|
||||
forceDownload := false
|
||||
if strings.HasPrefix(command, "d:") {
|
||||
command, _ = strings.CutPrefix(command, "d:")
|
||||
command = strings.TrimSpace(command)
|
||||
|
||||
forceDownload = true
|
||||
}
|
||||
|
||||
// fetch special music object
|
||||
if indexSelectionPattern.MatchString(command) {
|
||||
currentMusicObjects, err := store.currentMusicObjects()
|
||||
if err != nil {
|
||||
return store, err
|
||||
return store, []error{err}
|
||||
}
|
||||
|
||||
var fetched data.MusicObject
|
||||
@@ -96,14 +107,14 @@ func interpretCommand(command string, store musicObjectStore) (musicObjectStore,
|
||||
index, _ := strconv.Atoi(strings.TrimSpace(stringIndex))
|
||||
|
||||
if index >= len(currentMusicObjects) || index < 0 {
|
||||
return store, errors.New(command + " is out of bounds [0 <= " + strconv.Itoa(index) + " <= " + strconv.Itoa(len(currentMusicObjects)-1) + "]")
|
||||
return store, []error{errors.New(command + " is out of bounds [0 <= " + strconv.Itoa(index) + " <= " + strconv.Itoa(len(currentMusicObjects)-1) + "]")}
|
||||
}
|
||||
|
||||
current := currentMusicObjects[index]
|
||||
|
||||
newFetched, err := plugin.Fetch(current)
|
||||
if err != nil {
|
||||
return store, err
|
||||
return store, []error{err}
|
||||
}
|
||||
|
||||
if fetched == nil {
|
||||
@@ -113,7 +124,13 @@ func interpretCommand(command string, store musicObjectStore) (musicObjectStore,
|
||||
}
|
||||
}
|
||||
|
||||
return append(store, fetched.Related()), nil
|
||||
if forceDownload {
|
||||
return store, plugin.Download(fetched)
|
||||
} else {
|
||||
return append(store, fetched.Related()), []error{}
|
||||
}
|
||||
} else if forceDownload {
|
||||
return store, []error{errors.New("can only download indices not " + command)}
|
||||
}
|
||||
|
||||
// search in every other case
|
||||
@@ -123,30 +140,48 @@ func interpretCommand(command string, store musicObjectStore) (musicObjectStore,
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return append(store, currentMusicObjects), nil
|
||||
return append(store, currentMusicObjects), []error{}
|
||||
}
|
||||
|
||||
func Shell() {
|
||||
func Shell(commandsList ...[]string) {
|
||||
plugin.RegisterPlugin(&plugin.Musify{})
|
||||
|
||||
commands := []string{}
|
||||
if len(commandsList) > 0 {
|
||||
commands = commandsList[0]
|
||||
}
|
||||
|
||||
fmt.Println("== MusicKraken Shell ==")
|
||||
fmt.Println()
|
||||
|
||||
store := musicObjectStore{}
|
||||
var err error = nil
|
||||
|
||||
for {
|
||||
var command string
|
||||
|
||||
if len(commands) <= 0 {
|
||||
fmt.Print("> ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
command, err := reader.ReadString('\n')
|
||||
command, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
command = commands[0]
|
||||
commands = commands[1:]
|
||||
|
||||
store, err = interpretCommand(strings.TrimSpace(command), store)
|
||||
if err != nil {
|
||||
fmt.Println("> " + command)
|
||||
}
|
||||
|
||||
var errList []error
|
||||
store, errList = interpretCommand(strings.TrimSpace(command), store)
|
||||
if len(errList) > 0 {
|
||||
fmt.Println()
|
||||
for _, err := range errList {
|
||||
fmt.Println(color.Yellow + err.Error() + color.Reset)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -23,12 +22,103 @@ func ZeroPad(num int, length int) string {
|
||||
return strings.Repeat("0", length-len(str)) + str
|
||||
}
|
||||
|
||||
var numericRegex = regexp.MustCompile(`^[\d]+$`)
|
||||
|
||||
func IsNumeric(num string) bool {
|
||||
return numericRegex.MatchString(num)
|
||||
for _, c := range num {
|
||||
if c < '0' || c > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var commonTitleSuffix = []string{"(official video)"}
|
||||
|
||||
const openBrackets = "(["
|
||||
const closeBrackets = ")]"
|
||||
|
||||
var forbiddenSubstringInBrackets = []string{"official", "video", "audio", "lyrics", "prod", "remix", "ft", "feat", "ft.", "feat."}
|
||||
|
||||
func CleanSongTitle(title string, artistName string) string {
|
||||
/*
|
||||
# remove brackets and their content if they contain disallowed substrings
|
||||
for open_bracket, close_bracket in zip(OPEN_BRACKETS, CLOSE_BRACKETS):
|
||||
if open_bracket not in raw_song_title or close_bracket not in raw_song_title:
|
||||
continue
|
||||
|
||||
start = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
open_bracket_index = raw_song_title.index(open_bracket, start)
|
||||
except ValueError:
|
||||
break
|
||||
try:
|
||||
close_bracket_index = raw_song_title.index(close_bracket, open_bracket_index + 1)
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
substring = raw_song_title[open_bracket_index + 1:close_bracket_index]
|
||||
if any(disallowed_substring in substring.lower() for disallowed_substring in DISALLOWED_SUBSTRING_IN_BRACKETS):
|
||||
raw_song_title = raw_song_title[:open_bracket_index] + raw_song_title[close_bracket_index + 1:]
|
||||
else:
|
||||
start = close_bracket_index + 1
|
||||
|
||||
# everything that requires the artist name
|
||||
if artist_name is not None:
|
||||
artist_name = artist_name.strip()
|
||||
|
||||
# Remove artist from the start of the title
|
||||
if raw_song_title.lower().startswith(artist_name.lower()):
|
||||
|
||||
possible_new_name = raw_song_title[len(artist_name):].strip()
|
||||
|
||||
for char in ("-", "–", ":", "|"):
|
||||
if possible_new_name.startswith(char):
|
||||
raw_song_title = possible_new_name[1:].strip()
|
||||
break
|
||||
|
||||
return raw_song_title.strip()
|
||||
|
||||
*/
|
||||
|
||||
title = strings.TrimSpace(title)
|
||||
|
||||
for _, d := range commonTitleSuffix {
|
||||
if strings.HasSuffix(strings.ToLower(title), d) {
|
||||
title = strings.TrimSpace(title[:len(d)-1])
|
||||
}
|
||||
}
|
||||
|
||||
for b, open := range openBrackets {
|
||||
close := closeBrackets[b]
|
||||
|
||||
s := -1
|
||||
e := -1
|
||||
|
||||
for i, c := range title {
|
||||
if c == open {
|
||||
s = i
|
||||
} else if c == rune(close) {
|
||||
e = i
|
||||
}
|
||||
}
|
||||
|
||||
remove := false
|
||||
if s > -1 {
|
||||
substring := title[s:e]
|
||||
|
||||
for _, f := range forbiddenSubstringInBrackets {
|
||||
if strings.Contains(substring, f) {
|
||||
remove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if remove {
|
||||
title = title[:s] + title[e:]
|
||||
}
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ const ArtistSource = ObjectType("artist")
|
||||
|
||||
type Source struct {
|
||||
Url string
|
||||
AudioUrl string // will only be used when ObjectType = SongSource
|
||||
|
||||
SourceType *SourceType
|
||||
ObjectType ObjectType
|
||||
}
|
||||
@@ -38,6 +40,9 @@ func dedupeSources(inputSources []Source) []Source {
|
||||
if source.SourceType != nil {
|
||||
deduped[mergeWithIndex].SourceType = source.SourceType
|
||||
}
|
||||
if source.AudioUrl != "" {
|
||||
deduped[mergeWithIndex].AudioUrl = source.AudioUrl
|
||||
}
|
||||
|
||||
} else {
|
||||
// just appending
|
||||
|
||||
@@ -2,6 +2,7 @@ package plugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"gitea.elara.ws/Hazel/music-kraken/internal/common"
|
||||
@@ -163,7 +164,7 @@ func Fetch(musicObject data.MusicObject) (data.MusicObject, error) {
|
||||
musicObject = musicObject.Merge(newMusicObject)
|
||||
}
|
||||
|
||||
return musicObject, nil
|
||||
return musicObject.Compile(), nil
|
||||
}
|
||||
|
||||
type SearchConfig struct {
|
||||
@@ -189,3 +190,93 @@ func Search(search string, config SearchConfig) ([]data.MusicObject, error) {
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type downloadState struct {
|
||||
artist *data.Artist
|
||||
album *data.Album
|
||||
song *data.Song
|
||||
}
|
||||
|
||||
var variousArtist = data.Artist{
|
||||
Name: "VariousArtist",
|
||||
}.Compile().(data.Artist)
|
||||
|
||||
var variousAlbum = data.Album{
|
||||
Name: "VariousAlbum",
|
||||
}.Compile().(data.Album)
|
||||
|
||||
func downloadSong(song data.Song, state downloadState) error {
|
||||
fmt.Println("downloading: " + song.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Download(musicObject data.MusicObject, statesInput ...downloadState) []error {
|
||||
state := downloadState{}
|
||||
if len(statesInput) > 0 {
|
||||
state = statesInput[0]
|
||||
}
|
||||
|
||||
musicObject, err := Fetch(musicObject)
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
if song, ok := musicObject.(data.Song); ok {
|
||||
state.song = &song
|
||||
|
||||
if state.artist == nil {
|
||||
if len(song.Artists) > 0 {
|
||||
state.artist = &song.Artists[0]
|
||||
} else {
|
||||
state.artist = &variousArtist
|
||||
}
|
||||
}
|
||||
|
||||
if state.album == nil {
|
||||
if song.Album.Name != "" {
|
||||
state.album = &song.Album
|
||||
} else {
|
||||
state.album = &variousAlbum
|
||||
}
|
||||
}
|
||||
|
||||
err := downloadSong(song, state)
|
||||
if err == nil {
|
||||
return []error{}
|
||||
} else {
|
||||
return []error{err}
|
||||
}
|
||||
}
|
||||
|
||||
if album, ok := musicObject.(data.Album); ok {
|
||||
state.album = &album
|
||||
|
||||
if state.artist == nil {
|
||||
if len(album.Artists) > 0 {
|
||||
state.artist = &album.Artists[0]
|
||||
} else {
|
||||
state.artist = &variousArtist
|
||||
}
|
||||
}
|
||||
|
||||
errList := []error{}
|
||||
for _, song := range album.Songs {
|
||||
errList = append(errList, Download(song, state)...)
|
||||
}
|
||||
return errList
|
||||
}
|
||||
|
||||
if artist, ok := musicObject.(data.Artist); ok {
|
||||
state.artist = &artist
|
||||
|
||||
errList := []error{}
|
||||
for _, album := range artist.Albums {
|
||||
errList = append(errList, Download(album, state)...)
|
||||
}
|
||||
return errList
|
||||
}
|
||||
|
||||
return []error{
|
||||
errors.New("music object not recognized"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,39 @@ func (m *Musify) Search(query common.Query) ([]data.MusicObject, error) {
|
||||
return musicObjects, nil
|
||||
}
|
||||
|
||||
type parsedSongUrl struct {
|
||||
id string
|
||||
name string
|
||||
url string
|
||||
}
|
||||
|
||||
func newParsedSongUrl(rawUrl string) (parsedSongUrl, error) {
|
||||
res := parsedSongUrl{
|
||||
url: rawUrl,
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
dirs := strings.Split(parsed.Path, "/")
|
||||
correctPart := dirs[len(dirs)-1]
|
||||
split := strings.Split(correctPart, "-")
|
||||
if len(split) < 2 {
|
||||
return res, errors.New("last part of path has to consist of at least one - " + correctPart)
|
||||
}
|
||||
|
||||
res.id = strings.TrimSpace(split[len(split)-1])
|
||||
res.name = strings.Join(split[:len(split)-1], "-")
|
||||
|
||||
if !common.IsNumeric(res.id) {
|
||||
return res, errors.New("last elem (id) has to be numeric " + res.id)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *Musify) FetchSong(source data.Source) (data.Song, error) {
|
||||
song := data.Song{
|
||||
Sources: []data.Source{
|
||||
@@ -298,14 +331,19 @@ func (m *Musify) FetchSong(source data.Source) (data.Song, error) {
|
||||
}
|
||||
|
||||
// Download URL
|
||||
/*
|
||||
doc.Find("a[itemprop='audio']").Each(func(i int, anchor *goquery.Selection) {
|
||||
href, exists := anchor.Attr("href")
|
||||
if exists {
|
||||
source.AudioURL = p.host + href
|
||||
if href, _ := anchor.Attr("href"); true {
|
||||
// will be the source first added at the begining
|
||||
song.Sources[0].AudioUrl = musifyHost + href
|
||||
} else {
|
||||
// http://musify.club/track/dl/7141298/crystal-f-sekundenschlaf.mp3
|
||||
parsed, err := newParsedSongUrl(song.Sources[0].Url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
song.Sources[0].AudioUrl = "http://musify.club/track/dl/" + parsed.id + "/" + parsed.name + ".mp3"
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
// Song detail
|
||||
var listElement *goquery.Selection
|
||||
@@ -849,7 +887,7 @@ func (m Musify) parseAlbumCard(albumCard *goquery.Selection, artistName string)
|
||||
func (m *Musify) fetchArtistDiscography(url parsedArtistUrl, artistName string, albumTypeBlacklist []string) ([]data.Album, error) {
|
||||
albumList := []data.Album{}
|
||||
|
||||
endpoint := musifyHost + "/artist/filteralbums"
|
||||
endpoint := "https://musify.club/discography/filteralbums"
|
||||
|
||||
// POST request with form data
|
||||
formData := map[string]string{
|
||||
@@ -858,15 +896,11 @@ func (m *Musify) fetchArtistDiscography(url parsedArtistUrl, artistName string,
|
||||
"SortOrder.IsAscending": "false",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
}
|
||||
|
||||
resp, err := m.session.PostForm(endpoint, formData)
|
||||
resp, err := m.session.PostForm(endpoint, formData, map[string]string{"X-Requested-With": "XMLHttpRequest"})
|
||||
if err != nil {
|
||||
return albumList, err
|
||||
}
|
||||
|
||||
fmt.Println(scraper.GetText(resp))
|
||||
return albumList, nil
|
||||
|
||||
doc, err := scraper.GetHtml(resp)
|
||||
if err != nil {
|
||||
return albumList, err
|
||||
|
||||
@@ -127,8 +127,7 @@ func (s *Session) PostForm(rawUrl string, data map[string]string, headers ...map
|
||||
for k, v := range data {
|
||||
formData.Add(k, v)
|
||||
}
|
||||
body := strings.NewReader(formData.Encode())
|
||||
req, err := http.NewRequest("POST", fullURL, body)
|
||||
req, err := http.NewRequest("POST", fullURL, strings.NewReader(formData.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user