├── .gitignore ├── testing ├── .gitignore ├── test.mp3 ├── README.md └── testMuLi.sh ├── .qiave ├── brand.png ├── brandMin.png └── config ├── musicmgr ├── musicFiles.go └── mpeg.go ├── playlistmgr ├── README.md └── playlistFiles.go ├── fs.go ├── tools ├── scanner.go └── playlister.go ├── store ├── dropManager.go ├── README.md ├── moveManager.go ├── playlistManager.go └── objectmanager.go ├── dispatcher.go ├── main.go ├── README.md ├── LICENSE ├── file.go └── dir.go /.gitignore: -------------------------------------------------------------------------------- 1 | mulifs 2 | muli.db 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /testing/.gitignore: -------------------------------------------------------------------------------- 1 | testSrc/ 2 | testDst/ 3 | muli.db 4 | muli.log 5 | -------------------------------------------------------------------------------- /.qiave/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dankomiocevic/mulifs/HEAD/.qiave/brand.png -------------------------------------------------------------------------------- /testing/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dankomiocevic/mulifs/HEAD/testing/test.mp3 -------------------------------------------------------------------------------- /.qiave/brandMin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dankomiocevic/mulifs/HEAD/.qiave/brandMin.png -------------------------------------------------------------------------------- /.qiave/config: -------------------------------------------------------------------------------- 1 | [general] 2 | 3 | project_name=MuliFS 4 | slogan_subtitle=Music Library Filesystem 5 | project_first_letter=M 6 | button_path=https://github.com/dankomiocevic/mulifs 7 | button_label=Try it now 8 | load_hash=MuLiFS--Music-Library-Filesystem 9 | 10 | 11 | [colors] 12 | 13 | main_color=02b3d5 14 | main_color_hover=0288a3 15 | header_bg=02b3d5 16 | 17 | 18 | [links] 19 | 20 | link1_label=GitHub 21 | link1=https://github.com/dankomiocevic/mulifs 22 | link2_label=Doc 23 | link2=http://dankomiocevic.github.io/mulifs/#doc 24 | link3_label=Download 25 | link3=https://github.com/dankomiocevic/mulifs/archive/master.zip 26 | -------------------------------------------------------------------------------- /musicmgr/musicFiles.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | // Package musicmgr controls the tags in the music files. 18 | // The tools include tools to read the different Tags in the 19 | // music files. 20 | package musicmgr 21 | 22 | // FileTags defines the tags found in a specific music file. 23 | type FileTags struct { 24 | Title string 25 | Artist string 26 | Album string 27 | } 28 | -------------------------------------------------------------------------------- /playlistmgr/README.md: -------------------------------------------------------------------------------- 1 | How MuLi manages the Playlists? 2 | =============================== 3 | 4 | There is a special directory in MuLi structure called *playlists*, this directory manages music playlists in m3u format. 5 | The playlists are managed in directories like the rest of the filesystem, but it has some restrictions related to the files that the m3u file can contain, the main restriction is that it can only handle files that are already stored in the MuLi filesystem. 6 | 7 | Let's take a look at an example of a managed m3u file: 8 | 9 | ```ini 10 | #EXTM3U 11 | 12 | #MULI Some_Artist - Some_Album - Some_song 13 | /path/to/file/Some_song.mp3 14 | #MULI Some_Artist - Some_Album - Other_song 15 | /path/to/file/Other_song.mp3 16 | #MULI Some_Artist - Other_Album - Great_song 17 | /path/to/file/Great_song.mp3 18 | ``` 19 | 20 | Before each line there is a #MULI tag that defines where is the file located in the MuLi structure. This information is generated in order to maintain the data after the filesystem is unmounted. 21 | 22 | It is not advisable to modify the playlist from the external folder instead of using MuLi since it only updates the files when the filesystem is loaded. If there is a line that does not have a #MULI tag before the song path, that line will be ignored and will be deleted when the playlist is generated again. 23 | 24 | MuLi regenerates the playlists every time there is a change in one of the songs or there is a change in the playlist structure. 25 | -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | "syscall" 22 | 23 | "bazil.org/fuse" 24 | "bazil.org/fuse/fs" 25 | "golang.org/x/net/context" 26 | ) 27 | 28 | // FS struct holds information about the 29 | // entire filesystem. 30 | // It contains the mount point specified by the user. 31 | type FS struct { 32 | mPoint string 33 | } 34 | 35 | var _ = fs.FS(&FS{}) 36 | 37 | func (f *FS) Root() (fs.Node, error) { 38 | n := &Dir{ 39 | fs: f, 40 | artist: "", 41 | album: "", 42 | mPoint: f.mPoint, 43 | } 44 | return n, nil 45 | } 46 | 47 | func (f *FS) Statfs(ctx context.Context, req *fuse.StatfsRequest, resp *fuse.StatfsResponse) error { 48 | var stat syscall.Statfs_t 49 | wd, err := os.Getwd() 50 | if err != nil { 51 | return err 52 | } 53 | syscall.Statfs(wd, &stat) 54 | 55 | resp.Blocks = stat.Blocks 56 | resp.Bfree = stat.Bfree 57 | resp.Bavail = stat.Bavail 58 | resp.Bsize = uint32(stat.Bsize) 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /tools/scanner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | // Package tools contains different kind of tools to 18 | // manage the files in the filesystem. 19 | // The tools include tools to scan the Directories and SubDirectories in 20 | // the target path. 21 | package tools 22 | 23 | import ( 24 | "github.com/dankomiocevic/mulifs/musicmgr" 25 | "github.com/dankomiocevic/mulifs/store" 26 | "github.com/golang/glog" 27 | "os" 28 | "path/filepath" 29 | "strings" 30 | ) 31 | 32 | // visit checks that the specified file is 33 | // a music file and is on the correct path. 34 | // If it is ok, it stores it on the database. 35 | func visit(path string, f os.FileInfo, err error) error { 36 | if strings.HasSuffix(path, ".mp3") { 37 | glog.Infof("Reading %s\n", path) 38 | err, f := musicmgr.GetMp3Tags(path) 39 | if err != nil { 40 | glog.Errorf("Error in %s\n", path) 41 | } 42 | if f.Artist == "drop" { 43 | glog.Errorf("Error in %s\n", path) 44 | } 45 | if f.Artist == "playlists" { 46 | glog.Errorf("Error in %s\n", path) 47 | } 48 | store.StoreNewSong(&f, path) 49 | } 50 | return nil 51 | } 52 | 53 | // ScanFolder scans the specified root path 54 | // and SubDirectories searching for music files. 55 | // It uses filepath to walk through the file tree 56 | // and calls visit on every endpoint found. 57 | func ScanFolder(root string) error { 58 | err := filepath.Walk(root, visit) 59 | // TODO: Scan playlists 60 | return err 61 | } 62 | -------------------------------------------------------------------------------- /musicmgr/mpeg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | // Package musicmgr controls the tags in the music files. 18 | // The tools include tools to read the different Tags in the 19 | // music files. 20 | package musicmgr 21 | 22 | import ( 23 | "path/filepath" 24 | 25 | id3 "github.com/mikkyang/id3-go" 26 | ) 27 | 28 | // GetMp3Tags returns a FileTags struct with 29 | // all the information obtained from the tags in the 30 | // MP3 file. 31 | // Includes the Artist, Album and Song and defines 32 | // default values if the values are missing. 33 | // If the tags are missing, the default values will 34 | // be stored on the file. 35 | // If the tags are obtained correctly the first 36 | // return value will be nil. 37 | func GetMp3Tags(path string) (error, FileTags) { 38 | mp3File, err := id3.Open(path) 39 | if err != nil { 40 | _, file := filepath.Split(path) 41 | extension := filepath.Ext(file) 42 | songTitle := file[0 : len(file)-len(extension)] 43 | return err, FileTags{songTitle, "unknown", "unknown"} 44 | } 45 | 46 | defer mp3File.Close() 47 | 48 | title := mp3File.Title() 49 | if title == "" || title == "unknown" { 50 | _, file := filepath.Split(path) 51 | extension := filepath.Ext(file) 52 | title = file[0 : len(file)-len(extension)] 53 | mp3File.SetTitle(title) 54 | } 55 | 56 | artist := mp3File.Artist() 57 | if artist == "" { 58 | artist = "unknown" 59 | mp3File.SetArtist(artist) 60 | } 61 | 62 | album := mp3File.Album() 63 | if album == "" { 64 | album = "unknown" 65 | mp3File.SetAlbum(album) 66 | } 67 | 68 | ft := FileTags{title, artist, album} 69 | return nil, ft 70 | } 71 | 72 | // SetMp3Tags updates the Artist, Album and Title 73 | // tags with new values in the song MP3 file. 74 | func SetMp3Tags(artist string, album string, title string, songPath string) error { 75 | mp3File, err := id3.Open(songPath) 76 | if err != nil { 77 | return err 78 | } 79 | defer mp3File.Close() 80 | 81 | mp3File.SetTitle(title) 82 | mp3File.SetArtist(artist) 83 | mp3File.SetAlbum(album) 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /testing/README.md: -------------------------------------------------------------------------------- 1 | MuLiFS testing module 2 | ===================== 3 | 4 | The idea behind this testing module is to force MuLiFS to perform all the tasks it would do in a typical environment. 5 | 6 | As it is a FileSystem using unit testing was more like forcing a testing tool to something that is not right. To test MuLi it is necessary to initialize many parts and everything works as a group. 7 | 8 | I thought (and help me here if you have a better idea) that MuLi should be tested as a FileSystem and the best way to test it is creating a Bash script since it is the tool that will most probably manage MuLi. 9 | 10 | So I created a really long script that does all the things that MuLi would do in a real environment, those are listed in the following sections. 11 | 12 | Please feel free to add more testing code to the script and help me to improve this FileSystem, just send me a Pull Request. 13 | 14 | 15 | How it works 16 | ------------ 17 | There is an MP3 file that I created (some time ago for a background music in a game) and that file will be modified (change the Tags) in order to generate more files. 18 | 19 | Then the file will be copied around and modified doing all the necessary testing and checks. 20 | 21 | 22 | Testing modules 23 | --------------- 24 | The script will test the following: 25 | 26 | - Create lots of mp3 files and add different Tags in order to force MuLi to create the directory structure. 27 | - Check the .description files. **(WIP)** 28 | - Test with a file with special characters in the Tags. **(WIP)** 29 | - Test with a file with empty Tags. 30 | - Test the Copy command (Artists, Albums and songs). 31 | - Test the Rename command (Artists, Albums and songs). 32 | - Test the Delete command (Artists, Albums and songs). 33 | - Test the MkDir comand (Artists, Albums and songs). 34 | - Test the Drop directory (throw new files and existing files). 35 | - Test the Playlist Rename command (Artists, Albums and songs). **(WIP)** 36 | - Test the Playlist Copy command (Artists, Albums and songs). **(WIP)** 37 | - Test the Playlist Delete command (Artists, Albums and songs). **(WIP)** 38 | - Test the Playlist MkDir command (Artists, Albums and songs). 39 | - Test the Playlist Drop. 40 | - Test when files exists in MuLi and are dropped in drop and playlists directories. **(WIP)** 41 | 42 | 43 | Requirements 44 | ------------ 45 | The requirements to make this tool work are the following: 46 | 47 | - Have a MuLiFS compiled binary. 48 | - Have a ID3Tag tool installed. 49 | 50 | I am using MAC default id3 tool and the script works with that, if you are using a different one, there are three funcions in the script (strip_tags, set_tags and check_tags) that need to be modified in order to use the new tool. 51 | -------------------------------------------------------------------------------- /tools/playlister.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | // Package tools contains different kind of tools to 18 | // manage the files in the filesystem. 19 | // The tools include tools to scan the Directories and SubDirectories in 20 | // the target path. 21 | package tools 22 | 23 | import ( 24 | "github.com/dankomiocevic/mulifs/playlistmgr" 25 | "github.com/dankomiocevic/mulifs/store" 26 | "github.com/golang/glog" 27 | "io/ioutil" 28 | "os" 29 | "strings" 30 | ) 31 | 32 | // visitPlaylist checks that the specified file is 33 | // a music file and is on the correct path. 34 | // If it is ok, it stores it on the database. 35 | func visitPlaylist(name, path, mPoint string) error { 36 | if path[len(path)-1] != '/' { 37 | path = path + "/" 38 | } 39 | 40 | fullPath := path + name 41 | if strings.HasSuffix(fullPath, ".m3u") { 42 | glog.Infof("Reading %s\n", fullPath) 43 | err := playlistmgr.CheckPlaylistFile(fullPath) 44 | if err != nil { 45 | glog.Infof("Error in %s playlist\n", name) 46 | return err 47 | } 48 | 49 | files, err := playlistmgr.ProcessPlaylist(fullPath) 50 | if err != nil { 51 | glog.Infof("Problem reading playlist %s: %s\n", name, err) 52 | return err 53 | } 54 | 55 | playlistName := name[:len(name)-len(".m3u")] 56 | playlistName, _ = store.CreatePlaylist(playlistName, mPoint) 57 | 58 | for _, f := range files { 59 | store.AddFileToPlaylist(f, playlistName) 60 | } 61 | 62 | os.Remove(fullPath) 63 | store.RegeneratePlaylistFile(playlistName, mPoint) 64 | } 65 | return nil 66 | } 67 | 68 | // ScanPlaylistFolder scans the specified root path 69 | // and SubDirectories searching for playlist files. 70 | // It uses filepath to walk through the file tree 71 | // and calls visit on every endpoint found. 72 | func ScanPlaylistFolder(root string) error { 73 | if root[len(root)-1] != '/' { 74 | root = root + "/" 75 | } 76 | 77 | fullPath := root + "playlists/" 78 | files, _ := ioutil.ReadDir(fullPath) 79 | for _, f := range files { 80 | if !f.IsDir() { 81 | visitPlaylist(f.Name(), fullPath, root) 82 | } 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /store/dropManager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | package store 18 | 19 | import ( 20 | "errors" 21 | "github.com/dankomiocevic/mulifs/musicmgr" 22 | "github.com/golang/glog" 23 | "os" 24 | "path/filepath" 25 | 26 | "bazil.org/fuse" 27 | ) 28 | 29 | /** Deletes a file in the drop folder. 30 | */ 31 | func deleteDrop(path string) { 32 | os.Remove(path) 33 | } 34 | 35 | /** This function manages the Drop directory. 36 | * The user can copy/create files into this directory and 37 | * the files will be organized to the correct directory 38 | * based on the file tags. 39 | */ 40 | func HandleDrop(path, rootPoint string) error { 41 | glog.Infof("Handle drop with path: %s\n", path) 42 | err, fileTags := musicmgr.GetMp3Tags(path) 43 | if err != nil { 44 | deleteDrop(path) 45 | return fuse.EIO 46 | } 47 | 48 | extension := filepath.Ext(path) 49 | 50 | artist, err := CreateArtist(fileTags.Artist) 51 | if err != nil && err != fuse.EEXIST { 52 | glog.Infof("Error creating Artist: %s\n", err) 53 | return err 54 | } 55 | 56 | album, err := CreateAlbum(artist, fileTags.Album) 57 | if err != nil && err != fuse.EEXIST { 58 | glog.Infof("Error creating Album: %s\n", err) 59 | return err 60 | } 61 | 62 | //_, file := filepath.Split(path) 63 | newPath := rootPoint + artist + "/" + album + "/" 64 | os.MkdirAll(newPath, 0777) 65 | 66 | file := GetCompatibleString(fileTags.Title) + extension 67 | err = os.Rename(path, newPath+file) 68 | if err != nil { 69 | glog.Infof("Error renaming song: %s\n", err) 70 | return fuse.EIO 71 | } 72 | 73 | _, err = CreateSong(artist, album, fileTags.Title+extension, newPath) 74 | deleteDrop(path) 75 | if err != nil { 76 | glog.Infof("Error creating song in the DB: %s\n", err) 77 | } 78 | return err 79 | } 80 | 81 | /** Returns the path of a file in the drop directory. 82 | */ 83 | func GetDropFilePath(name, mPoint string) (string, error) { 84 | rootPoint := mPoint 85 | if rootPoint[len(rootPoint)-1] != '/' { 86 | rootPoint = rootPoint + "/" 87 | } 88 | 89 | path := rootPoint + "drop/" + name 90 | glog.Infof("Getting drop file path for: %s\n", path) 91 | // Check if the file exists 92 | src, err := os.Stat(path) 93 | if err == nil && src.IsDir() { 94 | glog.Info("File not found.") 95 | return "", errors.New("File not found.") 96 | } 97 | return path, err 98 | } 99 | -------------------------------------------------------------------------------- /dispatcher.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | ) 23 | 24 | /** FileItem struct contains three elements 25 | * the File object that needs to be processed, 26 | * the last time it was modified and the last 27 | * action performed over it. 28 | */ 29 | type FileItem struct { 30 | Fn func(File) error 31 | FileObject File 32 | Touched time.Time 33 | } 34 | 35 | var fileItems []FileItem 36 | var fChannel chan FileItem 37 | 38 | /** InitDispatcher initializes the 39 | * lists and channels to connect to the 40 | * dispatcher. It also inits the main loop. 41 | */ 42 | func InitDispatcher() { 43 | fileItems = make([]FileItem, 0, 20) 44 | fChannel = make(chan FileItem, 10) 45 | 46 | go processMsgs() 47 | } 48 | 49 | /** compareFiles compares two File structs 50 | * and returns true if the structs are the equal. 51 | */ 52 | func compareFiles(f1, f2 File) bool { 53 | return f1.artist == f2.artist && f1.album == f2.album && f1.song == f2.song && f1.name == f2.name 54 | } 55 | 56 | /** addFile adds a new FileItem to the list of 57 | * items that need to be processed once timed out. 58 | * If the element already exists on the list, it gets 59 | * updated. 60 | */ 61 | func addFile(f FileItem) { 62 | for count, item := range fileItems { 63 | if compareFiles(item.FileObject, f.FileObject) { 64 | fileItems[count].Touched = f.Touched 65 | if f.Fn != nil { 66 | fileItems[count].Fn = f.Fn 67 | } 68 | return 69 | } 70 | } 71 | fileItems = append(fileItems, f) 72 | } 73 | 74 | /** cleanLists checks that any of the file 75 | * elements has been timed out and runs 76 | * the correct action over it. 77 | * It also deletes the timed out elements 78 | * from the list. 79 | */ 80 | func cleanLists() { 81 | timeout := time.Now().Add(time.Second * -3) 82 | for i := len(fileItems) - 1; i >= 0; i-- { 83 | item := fileItems[i] 84 | if item.Touched.Before(timeout) { 85 | if item.Fn != nil { 86 | item.Fn(item.FileObject) 87 | } 88 | fileItems = append(fileItems[:i], fileItems[i+1:]...) 89 | } 90 | } 91 | } 92 | 93 | /** processMsgs receives all the messages 94 | * from the channels and process them. 95 | * This is the main loop of the dispatcher. 96 | */ 97 | func processMsgs() { 98 | for { 99 | select { 100 | case res := <-fChannel: 101 | addFile(res) 102 | case <-time.After(time.Second * 3): 103 | cleanLists() 104 | } 105 | } 106 | } 107 | 108 | /** PushFileItem receives a new File 109 | * to be processed in the near future. 110 | * The fn parameter is the function that 111 | * is going to been executed on the file. 112 | */ 113 | func PushFileItem(f File, fn func(File) error) { 114 | fmt.Printf("Push event for file\n") 115 | fileItem := FileItem{ 116 | Fn: fn, 117 | FileObject: f, 118 | Touched: time.Now(), 119 | } 120 | fChannel <- fileItem 121 | } 122 | -------------------------------------------------------------------------------- /playlistmgr/playlistFiles.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | // Package playlistmgr contains all the tools to read and modify 18 | // playlists files. 19 | package playlistmgr 20 | 21 | import ( 22 | "bufio" 23 | "errors" 24 | "fmt" 25 | "github.com/golang/glog" 26 | "os" 27 | "strings" 28 | ) 29 | 30 | // FileTags defines the tags found in a specific music file. 31 | type PlaylistFile struct { 32 | Title string 33 | Artist string 34 | Album string 35 | Path string 36 | } 37 | 38 | // CheckPlaylistFile opens a Playlist file and checks that 39 | // it really is a valid Playlist. 40 | func CheckPlaylistFile(path string) error { 41 | f, err := os.Open(path) 42 | 43 | if err != nil { 44 | return err 45 | } 46 | defer f.Close() 47 | 48 | firstLine := make([]byte, len("#EXTM3U")) 49 | _, err = f.Read(firstLine) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if string(firstLine) != "#EXTM3U" { 55 | return errors.New("Not a playlist!") 56 | } 57 | 58 | return nil 59 | } 60 | 61 | // ProcessPlaylist function receives the path of a playlist 62 | // and adds all the information into the database. 63 | // It process every line in the file and reads all the 64 | // songs in it. 65 | func ProcessPlaylist(path string) ([]PlaylistFile, error) { 66 | var a []PlaylistFile 67 | src, err := os.Stat(path) 68 | if err != nil || src.IsDir() { 69 | return a, errors.New("Playlist not found.") 70 | } 71 | 72 | file, err := os.Open(path) 73 | if err != nil { 74 | return a, errors.New("Cannot open playlist file.") 75 | } 76 | defer file.Close() 77 | 78 | scanner := bufio.NewScanner(file) 79 | 80 | for scanner.Scan() { 81 | line := scanner.Text() 82 | if strings.HasPrefix(line, "#MULI ") { 83 | line = line[len("#MULI "):] 84 | items := strings.Split(line, " - ") 85 | if len(items) != 3 { 86 | var playlistFile PlaylistFile 87 | playlistFile.Artist = items[0] 88 | playlistFile.Album = items[1] 89 | playlistFile.Title = items[2] 90 | a = append(a, playlistFile) 91 | } 92 | } 93 | } 94 | 95 | err = scanner.Err() 96 | return a, err 97 | } 98 | 99 | // DeletePlaylist deletes a playlist from the filesystem. 100 | func DeletePlaylist(playlist, mPoint string) error { 101 | if mPoint[len(mPoint)-1] != '/' { 102 | mPoint = mPoint + "/" 103 | } 104 | 105 | path := mPoint + "playlists/" + playlist 106 | src, err := os.Stat(path) 107 | if err == nil && src.IsDir() { 108 | os.Remove(path) 109 | } 110 | 111 | path = path + ".m3u" 112 | _, err = os.Stat(path) 113 | if err == nil { 114 | os.Remove(path) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | // RegeneratePlaylistFile creates the playlist file from the 121 | // information in the database. 122 | func RegeneratePlaylistFile(songs []PlaylistFile, playlist, mPoint string) error { 123 | glog.Infof("Regenerating playlist file for playlist: %s\n", playlist) 124 | if mPoint[len(mPoint)-1] != '/' { 125 | mPoint = mPoint + "/" 126 | } 127 | 128 | _, err := os.Stat(mPoint + "playlists") 129 | if err != nil { 130 | os.Mkdir(mPoint+"playlists/", 0777) 131 | } 132 | 133 | path := mPoint + "playlists/" + playlist + ".m3u" 134 | 135 | _, err = os.Stat(path) 136 | if err == nil { 137 | os.Remove(path) 138 | } 139 | 140 | f, err := os.Create(path) 141 | if err != nil { 142 | glog.Errorf("Error creating playlist file: %s\n", path) 143 | return err 144 | } 145 | defer f.Close() 146 | 147 | _, err = f.WriteString("#EXTM3U\n") 148 | if err != nil { 149 | glog.Infof("Cannot write on file.") 150 | } 151 | glog.Infof("Total songs: %d\n", len(songs)) 152 | for _, s := range songs { 153 | fmt.Printf("Adding song: %s\n", s.Title) 154 | _, err = f.WriteString("#MULI ") 155 | if err != nil { 156 | glog.Infof("Cannot write on file.") 157 | } 158 | _, err = f.WriteString(s.Artist) 159 | if err != nil { 160 | glog.Infof("Cannot write on file.") 161 | } 162 | _, err = f.WriteString(" - ") 163 | if err != nil { 164 | glog.Infof("Cannot write on file.") 165 | } 166 | _, err = f.WriteString(s.Album) 167 | if err != nil { 168 | glog.Infof("Cannot write on file.") 169 | } 170 | _, err = f.WriteString(" - ") 171 | if err != nil { 172 | glog.Infof("Cannot write on file.") 173 | } 174 | _, err = f.WriteString(s.Title) 175 | if err != nil { 176 | glog.Infof("Cannot write on file.") 177 | } 178 | _, err = f.WriteString("\n") 179 | if err != nil { 180 | glog.Infof("Cannot write on file.") 181 | } 182 | _, err = f.WriteString(s.Path) 183 | if err != nil { 184 | glog.Infof("Cannot write on file.") 185 | } 186 | _, err = f.WriteString("\n\n") 187 | if err != nil { 188 | glog.Infof("Cannot write on file.") 189 | } 190 | } 191 | 192 | f.Sync() 193 | return nil 194 | } 195 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "github.com/dankomiocevic/mulifs/store" 23 | "github.com/dankomiocevic/mulifs/tools" 24 | "log" 25 | "os" 26 | "path/filepath" 27 | "strconv" 28 | "strings" 29 | 30 | "bazil.org/fuse" 31 | "bazil.org/fuse/fs" 32 | ) 33 | 34 | type fs_config struct { 35 | uid uint 36 | gid uint 37 | allow_users bool 38 | allow_root bool 39 | } 40 | 41 | var config_params fs_config 42 | var progName = filepath.Base(os.Args[0]) 43 | 44 | const progVer = "0.1" 45 | 46 | // usage specifies how the command should be called 47 | // showing a message on the standard output. 48 | func usage() { 49 | fmt.Fprintf(os.Stderr, "Name:\n") 50 | fmt.Fprintf(os.Stderr, " %s %s\n", progName, progVer) 51 | fmt.Fprintf(os.Stderr, "\nSynopsis:\n") 52 | fmt.Fprintf(os.Stderr, " %s [global_options] MUSIC_SOURCE MOUNTPOINT \n", progName) 53 | fmt.Fprintf(os.Stderr, "\nDescription:\n") 54 | fmt.Fprintf(os.Stderr, " Mounts a filesystem in MOUNTPOINT with the music files obtained\n") 55 | fmt.Fprintf(os.Stderr, " from MUSIC_SOURCE ordered in folders by Artist and Album.\n") 56 | fmt.Fprintf(os.Stderr, "\n For more information please visit:\n") 57 | fmt.Fprintf(os.Stderr, " \n") 58 | fmt.Fprintf(os.Stderr, "\nParams:\n") 59 | fmt.Fprintf(os.Stderr, " MUSIC_SOURCE: The path of the folder containing the music files.\n") 60 | fmt.Fprintf(os.Stderr, " MOUNTPOINT: The path where MuLi should be mounted.\n") 61 | fmt.Fprintf(os.Stderr, "\nGlobal Options:\n") 62 | flag.PrintDefaults() 63 | fmt.Fprintf(os.Stderr, "\n") 64 | } 65 | 66 | func newTrue() *bool { 67 | b := true 68 | return &b 69 | } 70 | 71 | func main() { 72 | log.SetFlags(0) 73 | log.SetPrefix(progName + ": ") 74 | 75 | flag.Usage = usage 76 | var err error 77 | var db_path string 78 | var mount_ops string 79 | flag.StringVar(&db_path, "db_path", "muli.db", "Database path.") 80 | flag.StringVar(&mount_ops, "o", "", "Default mount options.") 81 | uid_conf := flag.Uint("uid", 0, "User owner of the files.") 82 | gid_conf := flag.Uint("gid", 0, "Group owner of the files.") 83 | allow_other := flag.Bool("allow_other", false, "Allow other users to access the filesystem.") 84 | allow_root := flag.Bool("allow_root", false, "Allow root to access the filesystem.") 85 | 86 | flag.Parse() 87 | 88 | if len(mount_ops) < 1 && flag.NArg() > 3 { 89 | for index, marg := range flag.Args() { 90 | if strings.Compare(marg, "-o") == 0 { 91 | mount_ops = flag.Arg(index + 1) 92 | break 93 | } 94 | } 95 | } 96 | 97 | 98 | if len(os.Getenv("PATH")) < 1 { 99 | os.Setenv("PATH", "/bin:/sbin") 100 | } 101 | 102 | if len(mount_ops) > 0 { 103 | opts_tokens := strings.Split(mount_ops, ",") 104 | for _, token := range opts_tokens { 105 | if strings.Compare(token, "allow_root") == 0 { 106 | allow_root = newTrue() 107 | } else if strings.Compare(token, "allow_other") == 0 { 108 | allow_other = newTrue() 109 | } else if strings.HasPrefix(token, "uid=") { 110 | parsed_uid, err := strconv.ParseUint(token[len("uid="):], 10, 32) 111 | if err != nil { 112 | log.Fatal(err) 113 | os.Exit(1) 114 | } else { 115 | uint_uid := uint(parsed_uid) 116 | uid_conf = &uint_uid 117 | } 118 | } else if strings.HasPrefix(token, "gid=") { 119 | parsed_gid, err := strconv.ParseUint(token[len("gid="):], 10, 32) 120 | if err != nil { 121 | log.Fatal(err) 122 | os.Exit(1) 123 | } else { 124 | uint_gid := uint(parsed_gid) 125 | gid_conf = &uint_gid 126 | } 127 | } else if strings.HasPrefix(token, "db_path=") { 128 | db_path = token[len("db_path="):] 129 | if len(db_path) < 3 { 130 | log.Fatal("Error in db_path") 131 | os.Exit(1) 132 | } 133 | } 134 | } 135 | } 136 | 137 | config_params = fs_config{ 138 | uid: *uid_conf, gid: *gid_conf, allow_users: *allow_other, allow_root: *allow_root, 139 | } 140 | 141 | if flag.NArg() < 2 { 142 | usage() 143 | os.Exit(2) 144 | } 145 | path := flag.Arg(0) 146 | mountpoint := flag.Arg(1) 147 | 148 | if path[0] == '-' { 149 | usage() 150 | os.Exit(3) 151 | } 152 | 153 | if mountpoint[0] == '-' { 154 | usage() 155 | os.Exit(4) 156 | } 157 | 158 | err = store.InitDB(db_path) 159 | if err != nil { 160 | log.Fatal(err) 161 | os.Exit(5) 162 | } 163 | 164 | path, err = filepath.Abs(path) 165 | if err != nil { 166 | log.Fatal(err) 167 | os.Exit(6) 168 | } 169 | 170 | err = tools.ScanFolder(path) 171 | if err != nil { 172 | log.Fatal(err) 173 | os.Exit(7) 174 | } 175 | 176 | err = tools.ScanPlaylistFolder(path) 177 | if err != nil { 178 | log.Fatal(err) 179 | os.Exit(8) 180 | } 181 | 182 | // Init the dispatcher system to process 183 | // delayed events. 184 | InitDispatcher() 185 | 186 | if err = mount(path, mountpoint); err != nil { 187 | log.Fatal(err) 188 | os.Exit(9) 189 | } 190 | } 191 | 192 | // mount calls the fuse library to specify 193 | // the details of the mounted filesystem. 194 | func mount(path, mountpoint string) error { 195 | // TODO: Check that there is no folder named 196 | 197 | mountOptions := []fuse.MountOption{ 198 | fuse.FSName("MuLi"), 199 | fuse.Subtype("MuLiFS"), 200 | fuse.LocalVolume(), 201 | fuse.VolumeName("Music Library"), 202 | } 203 | 204 | if config_params.allow_users { 205 | mountOptions = append(mountOptions, fuse.AllowOther()) 206 | } else { 207 | if config_params.allow_root { 208 | mountOptions = append(mountOptions, fuse.AllowRoot()) 209 | } 210 | } 211 | // playlist or drop in the path. 212 | c, err := fuse.Mount( 213 | mountpoint, mountOptions...) 214 | 215 | if err != nil { 216 | return err 217 | } 218 | defer c.Close() 219 | 220 | filesys := &FS{ 221 | mPoint: path, 222 | } 223 | 224 | if err := fs.Serve(c, filesys); err != nil { 225 | return err 226 | } 227 | 228 | // check if the mount process has an error to report 229 | <-c.Ready 230 | if err := c.MountError; err != nil { 231 | return err 232 | } 233 | 234 | return nil 235 | } 236 | -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | How MuLi stores the information? 2 | ================================ 3 | 4 | MuLi keeps a database with all the information about the Songs, Albums and Artists in a [BoltDB](https://github.com/boltdb/bolt). 5 | BoltDB is a simple yet powerful key/value store written in Go, as MuLi does not perform any query or search function and only keeps 6 | of the organization of the Music Library structure Bolt is the best choice for the task. 7 | 8 | 9 | Structure 10 | --------- 11 | 12 | The store is organized in the same way than the file structure. Bolt allow us to write Keys/Values and Buckets: 13 | 14 | * Key/Value: Is a simple storage of a specific Value (for example Artist Information) under the track of a specific Key 15 | (for example the Artist name). 16 | * Buckets: The buckets work like directories, they allow to store Key/Values and other Buckets under a specific Key. For 17 | example we store the Albums inside an Artist bucket. 18 | 19 | The structure we are using to organize the files is like the following: 20 | 21 | ``` 22 | Artists (Bucket) 23 | │ 24 | ├── Some_Artist (Bucket) 25 | │ ├── .description (Key/Value) 26 | │ │ 27 | │ ├── Some_Album (Bucket) 28 | │ │ ├── .description (Key/Value) 29 | │ │ └── Some_song (Key/Value) 30 | │ │ 31 | │ └── Other_Album (Bucket) 32 | │ ├── .description (Key/Value) 33 | │ ├── More_songs (Key/Value) 34 | │ ├── ... 35 | │ └── Other_song (Key/Value) 36 | │ 37 | └── Other_Artist (Bucket) 38 | ├── .description (Key/Value) 39 | │ 40 | └── Some_Album (Bucket) 41 | ├── .description (Key/Value) 42 | ├── Great_Song (Key/Value) 43 | ├── ... 44 | └── AwesomeSong (Key/Value) 45 | 46 | ``` 47 | 48 | The following statements are true: 49 | 50 | * All the Buckets are inside a root Bucket called "Artists", this is the main Bucket where all the others are located. 51 | * Every Key inside "Artists" is an Artist and is a Bucket, not a Key/Value. 52 | * Every Artist Bucket contains Album Buckets and a ".description" Key/Value with the information of the Artist. 53 | * Every Album Bucket contains Song Key/Values and a ".description" Key/Value with the information of the Album. 54 | 55 | 56 | Opening the store 57 | ----------------- 58 | Here is a short snippet that shows how to open the Bolt store: 59 | 60 | ```Go 61 | db, err := bolt.Open("db/path/muli.db", 0600, nil) 62 | if err != nil { 63 | return nil, err 64 | } 65 | defer db.Close() 66 | ``` 67 | 68 | 69 | Reading the Artists 70 | ------------------- 71 | 72 | The following is a snippet that explains how to read all the Artists in the store using Bolt: 73 | 74 | ```Go 75 | err := db.View(func(tx *bolt.Tx) error { 76 | // First, get the root Bucket (Artists) 77 | b := tx.Bucket([]byte("Artists")) 78 | // Create a cursor to Iterate the values. 79 | c := b.Cursor() 80 | for k, v := c.First(); k != nil; k, v = c.Next() { 81 | // When the value is nil, it is a Bucket. 82 | if v == nil { 83 | fmt.Printf("Artist: %s\n", k) 84 | } 85 | } 86 | return nil 87 | }) 88 | ``` 89 | This code will print all the Buckets (nil value) inside Artists Bucket. 90 | 91 | 92 | Reading the Artist information 93 | ------------------------------ 94 | 95 | The following snippet shows how to read the information for an Artist: 96 | 97 | ```Go 98 | err = db.View(func(tx *bolt.Tx) error { 99 | // First, get the root bucket (Artists) 100 | root := tx.Bucket([]byte("Artists")) 101 | // Now get the specific Artist Bucket 102 | // inside the previous one. 103 | b := root.Bucket([]byte("Some_Artist")) 104 | if b == nil { 105 | return errors.New("Artist not found.") 106 | } 107 | 108 | // Now get the description JSON 109 | artistJson := b.Get([]byte(".description")) 110 | if artistJson == nil { 111 | return errors.New("Description not found.") 112 | } 113 | 114 | // Of course, the JSON will need some processing 115 | // to get the values, here we just print it. 116 | fmt.Printf("Description: %s\n", artistJson) 117 | return nil 118 | }) 119 | ``` 120 | 121 | The Artist information is a JSON containing the Real Artist Name (the one with the special characters), 122 | the Directory Artist Name (the modified one that is compatible with most filesystems) and an array with 123 | all the Albums this Artist has. 124 | 125 | For example: 126 | ```json 127 | { 128 | "ArtistName":"Some Artist", 129 | "ArtistPath":"Some_Artist", 130 | "ArtistAlbums": 131 | [ 132 | "Some_Album", 133 | "Other_Album" 134 | ] 135 | } 136 | ``` 137 | 138 | Reading the Albums 139 | ------------------ 140 | 141 | The following snippet lists the Albums for an Artist: 142 | ```Go 143 | err := db.View(func(tx *bolt.Tx) error { 144 | // First, get the root Bucket (Artists) 145 | root := tx.Bucket([]byte("Artists")) 146 | // Now get the specific Artist Bucket 147 | // inside the previous one. 148 | b := root.Bucket([]byte("Some_Artist")) 149 | if b == nil { 150 | return errors.New("Artist not found.") 151 | } 152 | 153 | // Create a cursor to Iterate the values. 154 | c := b.Cursor() 155 | for k, v := c.First(); k != nil; k, v = c.Next() { 156 | // When the value is nil, it is a Bucket. 157 | if v == nil { 158 | fmt.Printf("Album: %s\n", k) 159 | } 160 | } 161 | return nil 162 | }) 163 | ``` 164 | 165 | 166 | Reading an Album description 167 | ---------------------------- 168 | 169 | The following snippet shows how to read the information for an Album: 170 | 171 | ```Go 172 | err = db.View(func(tx *bolt.Tx) error { 173 | // First, get the root bucket (Artists) 174 | root := tx.Bucket([]byte("Artists")) 175 | // Now get the specific Artist Bucket 176 | // inside the previous one. 177 | b := root.Bucket([]byte("Some_Artist")) 178 | if b == nil { 179 | return errors.New("Artist not found.") 180 | } 181 | 182 | // Then, get the specific Album Bucket 183 | // inside the previous one. 184 | c := root.Bucket([]byte("Other_Album")) 185 | if c == nil { 186 | return errors.New("Album not found.") 187 | } 188 | 189 | // Now get the description JSON 190 | albumJson := c.Get([]byte(".description")) 191 | if albumJson == nil { 192 | return errors.New("Description not found.") 193 | } 194 | 195 | // Of course, the JSON will need some processing 196 | // to get the values, here we just print it. 197 | fmt.Printf("Description: %s\n", albumJson) 198 | return nil 199 | }) 200 | ``` 201 | 202 | The Album information is a JSON containing the Real Album Name (the one with the special characters) and 203 | the Directory Album Name (the modified one that is compatible with most filesystems). 204 | 205 | For example: 206 | ```json 207 | { 208 | "AlbumName":"Other Album", 209 | "AlbumPath":"OtherAlbum" 210 | } 211 | ``` 212 | 213 | 214 | Reading the Songs in an Album 215 | ----------------------------- 216 | 217 | This code iterates through all the songs in an album: 218 | ```Go 219 | err := db.View(func(tx *bolt.Tx) error { 220 | // First, get the root Bucket (Artists) 221 | root := tx.Bucket([]byte("Artists")) 222 | // Now get the specific Artist Bucket 223 | // inside the previous one. 224 | b := root.Bucket([]byte("Some_Artist")) 225 | if b == nil { 226 | return errors.New("Artist not found.") 227 | } 228 | 229 | // Then, get the specific Album Bucket 230 | // inside the previous one. 231 | c := root.Bucket([]byte("Other_Album")) 232 | if c == nil { 233 | return errors.New("Album not found.") 234 | } 235 | 236 | // Create a cursor to Iterate the values. 237 | d := c.Cursor() 238 | for k, v := d.First(); k != nil; k, v = d.Next() { 239 | // Skip the description and nil values 240 | if v != nil && k != ".description" { 241 | fmt.Printf("Song: %s\n", k) 242 | fmt.Printf("Description: %s\n", v) 243 | } 244 | } 245 | return nil 246 | }) 247 | ``` 248 | 249 | All the Song values contain a JSON object with information about the Song, 250 | Real Name and File Name. 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MuLiFS : Music Library Filesystem 2 | ================================= 3 | 4 | [![GoDoc](https://godoc.org/github.com/dankomiocevic/mulifs?status.svg)](https://godoc.org/github.com/dankomiocevic/mulifs) 5 | 6 | MuLi (pronounced Moo-Lee) is a filesystem written in Go to mount music 7 | libraries and organize the music based on the music file tags. 8 | 9 | It scans a Directory tree and reads all the Tags in the music files and 10 | generates a Directory structure organizing the songs by Artist and 11 | Album. 12 | 13 | Quick Start 14 | ----------- 15 | 16 | For the anxious that don't like to read, here is the command to make 17 | this work: 18 | 19 | ``` 20 | mulifs MUSIC_SOURCE MOUNTPOINT 21 | ``` 22 | 23 | Where the MUSIC_SOURCE is the path where the music is stored and 24 | MOUNTPOINT is the path where MuLi should be mounted. 25 | 26 | 27 | Project status 28 | -------------- 29 | 30 | This project is currently under development and it is not ready to use yet. 31 | The basic functionality is ready but some work needs to be done, including: 32 | 33 | * Finish testing situations. 34 | * Enable drop directory to receive full directories and not only files. 35 | * Test and test! 36 | 37 | 38 | How it works 39 | ------------ 40 | 41 | Organizing a Music library is always a tedious task and there is always 42 | lots of different information that does not match. 43 | 44 | MuLi reads a Directory tree (Directories and Subdirectories of a specific 45 | path) and scans for all the music files (it actually supports only MP3, but more 46 | formats will be added). 47 | Every time it finds a music file it reads the ID Tags that specify the 48 | Artist, Album and Song name. 49 | If any of these parameters is missing it completes the information with 50 | default values (unknown Artist or Album and tries to read the song name 51 | from the path) and updates the Tags for future scans. 52 | It stores all the gathered information into a BoltDB that is an object 53 | store that is fast, simple and completely written in Go, that makes 54 | MuLi portable! 55 | 56 | Once the Directory is completely scanned and all the information is 57 | in the Database, MuLi creates a directory structure as following: 58 | 59 | ``` 60 | mounted_path 61 | │ 62 | ├── Some_Artist 63 | │ │ 64 | │ ├── Some_Album 65 | │ │ └── Some_song.mp3 66 | │ │ 67 | │ └── Other_Album 68 | │ ├── More_songs.mp3 69 | │ ├── ... 70 | │ └── Other_song.mp3 71 | │ 72 | ├── Other_Artist 73 | │ │ 74 | │ └── Some_Album 75 | │ ├── Great_Song.mp3 76 | │ ├── ... 77 | │ └── AwesomeSong.mp3 78 | │ 79 | ├── drop 80 | │ 81 | └── playlists 82 | │ 83 | └── Music_I_Like 84 | ├── Great_Song.mp3 85 | ├── AwesomeSong.mp3 86 | ├── ... 87 | └── Other_song.mp3 88 | 89 | ``` 90 | Lets take a look at this Directory structure! 91 | 92 | The first thing to notice is that in the root directory of the mounted 93 | path (the path where the filesystem is mounted), there are folders 94 | with the Artists names, one folder per Artist. 95 | 96 | When MuLi scans the music files to get the Tags information it changes 97 | the names to make them compatible with every operative system and filesystem. 98 | It removes the special characters and replaces the spaces with underscores, 99 | but only in the Directory and Files names. It does not modify the 100 | real names stored in the music files! 101 | 102 | Inside every Artist song there are Directories that match every Album 103 | in the Music Library, as it happens in the Artists Directory names, the 104 | names in the Albums are also modified. 105 | 106 | Finally, inside every Album are the Songs! The songs can be read, moved, 107 | modified and deleted without any problem. But be careful! When the Song 108 | is deleted, it is deleted from the origin path too!! 109 | 110 | When a Song is moved from one path to another inside the MuLi filesystem, 111 | the Tags inside the Song file are also updated. This makes the Music 112 | Library consistent and keeps every Song updated! 113 | If you create or copy a new Song file inside any folder, the Tags inside the 114 | file will be modified accordingly. 115 | 116 | Directories and Songs can be created and moved and it modifies the Tags 117 | on the Songs and creates or modifies Artists and Albums. 118 | Again, be careful! If you delete a Directory it will be PERMANENT for the 119 | Songs inside it! 120 | 121 | There are two special directories in the filesystem: 122 | 123 | 1. drop: Every file that is stored here will be scanned and moved to the 124 | correct location depending on the Tags it contains. If you have a new file 125 | that you want to add to the Music Library and you don't want to create 126 | the parent Directories, just drop it here! 127 | 128 | 2. playlists: This Directory manages the playlists, for every playlist 129 | in the Source Directory, all the files inside it are analyzed and 130 | the same Directory structure will be created. Then a playlist will 131 | be a Directory with the music files. The format 132 | used in playlists is M3U. 133 | 134 | 135 | Description files 136 | ----------------- 137 | 138 | As MuLi modifies the names for the Tags to be compatible with different 139 | filesystems, it also generates a special file inside every Directory 140 | called .description. 141 | 142 | The description files are the only files allowed to start with a dot in 143 | the MuLi filesystem, it contains a JSON with the information of the 144 | containing Album or Artist. 145 | 146 | For example, in the previous file structure, the Some_Artist directory 147 | would contain a Description file as this one: 148 | 149 | ```json 150 | { 151 | "ArtistName":"Some Artist", 152 | "ArtistPath":"Some_Artist", 153 | "ArtistAlbums": 154 | [ 155 | "Some_Album", 156 | "Other_Album" 157 | ] 158 | } 159 | ``` 160 | Here all the information for the Artist can be processed and read. 161 | On the other hand, for the Album Directories the example would be 162 | like this one in the Other_Album folder: 163 | 164 | ```json 165 | { 166 | "AlbumName":"Other Album", 167 | "AlbumPath":"OtherAlbum" 168 | } 169 | ``` 170 | 171 | Every special character will be removed, also the dots and the spaces 172 | are replaced with underscores. 173 | 174 | 175 | Information Storage 176 | ------------------- 177 | 178 | All the information about the Music Library that MuLi uses and gathers is 179 | stored in a [Bolt](https://github.com/boltdb/bolt) database (or Object store 180 | if you prefer). 181 | More information about it [here](https://github.com/dankomiocevic/mulifs/tree/master/store) 182 | 183 | 184 | Requirements 185 | ------------ 186 | 187 | * A computer running a flavour of *nix 188 | 189 | 190 | Dependencies 191 | ------------ 192 | 193 | * [github.com/bazil/fuse](https://github.com/bazil/fuse) 194 | * [github.com/boltdb/bolt](https://github.com/boltdb/bolt) 195 | * [github.com/golang/glog](https://github.com/golang/glog) 196 | 197 | MuLi is based on the awesome [Bazil's](https://github.com/bazil) 198 | implementation of [FUSE](https://github.com/bazil/fuse) purely in Go. 199 | It uses FUSE to generate the filesystem in userspace. 200 | 201 | It also uses the great and simple [BoltDB](https://github.com/boltdb/bolt) 202 | to store all the information of Songs, Artists and Albums. 203 | 204 | To manage the logs it uses the Glog library for Go. 205 | 206 | If you don't know these projects take a look at them! 207 | 208 | 209 | Installation (if you are not familiar with Go) 210 | ---------------------------------------------- 211 | 212 | 1. Follow this link to install Go and set up your environment: 213 | 214 | [https://golang.org/doc/install](https://golang.org/doc/install) 215 | (don't forget to set up your GOPATH) 216 | 217 | 2. Download, compile and install MuLi by running the following command: 218 | 219 | ``` 220 | go get github.com/dankomiocevic/mulifs 221 | ``` 222 | 223 | 224 | Running MuLi 225 | ------------ 226 | 227 | To start the Music Library Filesystem run: 228 | 229 | ``` 230 | mulifs [global_options] MUSIC_SOURCE MOUNTPOINT 231 | ``` 232 | 233 | 234 | ### Params ### 235 | * MUSIC_SOURCE: The path of the folder containing the music files. 236 | * MOUNTPOINT: The path where MuLi should be mounted. 237 | 238 | ### Global Options ### 239 | * allow_other: Allow other users to access the filesystem. 240 | * allow_root: Allow root to access the filesystem. 241 | * alsologtostderr: log to standard error as well as files 242 | * db_path string: Database path. (default "muli.db") 243 | * gid: An unsigned integer representing the Group that will own the files. 244 | * log_backtrace_at value: when logging hits line file:N, emit a stack trace (default :0) 245 | * log_dir string: If non-empty, write log files in this directory 246 | * logtostderr: log to standard error instead of files 247 | * stderrthreshold value: logs at or above this threshold go to stderr 248 | * uid: An unsigned integer representing the User that will own the files. 249 | * v value: log level for V logs 250 | * vmodule value: comma-separated list of pattern=N settings for file-filtered logging 251 | 252 | 253 | ToDo 254 | ---- 255 | - Playlists manager **(WIP)** 256 | - Heavy testing! (I mean, testing routines, testing functions, all the testing stuff!) **(WIP)** 257 | - Add the ability to drop entire folders to the drop folder. 258 | - Refactoring the code 259 | - This is my first project with Filesystems and Go, I learnt a lot but I created a lot of duplicated code and bad programming practices. This needs to be improved. 260 | 261 | 262 | License 263 | ------- 264 | 265 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 266 | 267 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 268 | 269 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 270 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Danko Miocevic 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "{}" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright {yyyy} {name of copyright owner} 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "github.com/dankomiocevic/mulifs/musicmgr" 23 | "github.com/dankomiocevic/mulifs/playlistmgr" 24 | "github.com/dankomiocevic/mulifs/store" 25 | "github.com/golang/glog" 26 | "io" 27 | "os" 28 | "path/filepath" 29 | "runtime" 30 | "strings" 31 | 32 | "bazil.org/fuse" 33 | "bazil.org/fuse/fs" 34 | "golang.org/x/net/context" 35 | ) 36 | 37 | // File defines a file in the filesystem structure. 38 | // files can be Songs or .description files. 39 | // Songs are actual songs in the Music Library and 40 | // .description files detail more information about the 41 | // Directory they are located in. 42 | type File struct { 43 | artist string 44 | album string 45 | song string 46 | name string 47 | mPoint string 48 | } 49 | 50 | /** This function is used to do nothing to the file 51 | * but to update the Touch time. 52 | */ 53 | func DelayedVoid(f File) error { 54 | return nil 55 | } 56 | 57 | func (f *File) Attr(ctx context.Context, a *fuse.Attr) error { 58 | glog.Infof("Entering file Attr with name: %s, Artist: %s and Album: %s.\n", f.name, f.artist, f.album) 59 | if f.name[0] == '.' { 60 | if f.name == ".description" { 61 | descriptionJson, err := store.GetDescription(f.artist, f.album, f.name) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | a.Size = uint64(len(descriptionJson)) 67 | a.Mode = 0444 68 | if config_params.uid != 0 { 69 | a.Uid = uint32(config_params.uid) 70 | } 71 | if config_params.gid != 0 { 72 | a.Gid = uint32(config_params.gid) 73 | } 74 | } else { 75 | return fuse.EPERM 76 | } 77 | } else { 78 | var songPath string 79 | var err error 80 | if f.artist == "drop" { 81 | songPath, err = store.GetDropFilePath(f.name, f.mPoint) 82 | PushFileItem(*f, nil) 83 | } else if f.artist == "playlists" { 84 | songPath, err = store.GetPlaylistFilePath(f.album, f.name, f.mPoint) 85 | PushFileItem(*f, nil) 86 | } else { 87 | songPath, err = store.GetFilePath(f.artist, f.album, f.name) 88 | } 89 | 90 | if err != nil { 91 | glog.Infof("Error getting song path: %s\n", err) 92 | return err 93 | } 94 | 95 | r, err := os.Open(songPath) 96 | if err != nil { 97 | glog.Infof("Error opening file: %s\n", err) 98 | return err 99 | } 100 | defer r.Close() 101 | 102 | fi, err := r.Stat() 103 | if err != nil { 104 | glog.Infof("Error getting file status: %s\n", err) 105 | return err 106 | } 107 | 108 | a.Size = uint64(fi.Size()) 109 | a.Mode = 0777 110 | if config_params.uid != 0 { 111 | a.Uid = uint32(config_params.uid) 112 | } 113 | if config_params.gid != 0 { 114 | a.Gid = uint32(config_params.gid) 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | var _ = fs.NodeOpener(&File{}) 121 | 122 | func (f *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { 123 | glog.Infof("Entered Open with file name: %s.\n", f.name) 124 | 125 | if f.name == ".description" { 126 | return &FileHandle{r: nil, f: f}, nil 127 | } 128 | 129 | if f.name[0] == '.' { 130 | return nil, fuse.EPERM 131 | } 132 | 133 | if runtime.GOOS == "darwin" { 134 | resp.Flags |= fuse.OpenDirectIO 135 | } 136 | 137 | if req.Flags.IsReadOnly() { 138 | glog.Info("Open: File requested is read only.\n") 139 | } 140 | if req.Flags.IsReadWrite() { 141 | glog.Info("Open: File requested is read write.\n") 142 | } 143 | if req.Flags.IsWriteOnly() { 144 | glog.Info("Open: File requested is write only.\n") 145 | } 146 | 147 | var err error 148 | var songPath string 149 | if f.artist == "drop" { 150 | songPath, err = store.GetDropFilePath(f.name, f.mPoint) 151 | PushFileItem(*f, DelayedVoid) 152 | } else if f.artist == "playlists" { 153 | songPath, err = store.GetPlaylistFilePath(f.album, f.name, f.mPoint) 154 | PushFileItem(*f, DelayedVoid) 155 | } else { 156 | songPath, err = store.GetFilePath(f.artist, f.album, f.name) 157 | } 158 | if err != nil { 159 | glog.Error(err) 160 | return nil, err 161 | } 162 | 163 | r, err := os.OpenFile(songPath, int(req.Flags), 0666) 164 | if err != nil { 165 | return nil, err 166 | } 167 | return &FileHandle{r: r, f: f}, nil 168 | } 169 | 170 | type FileHandle struct { 171 | r *os.File 172 | f *File 173 | } 174 | 175 | var _ fs.Handle = (*FileHandle)(nil) 176 | 177 | // DelayedHandlePlaylistSong handles a dropped file 178 | // inside a playlist and is called by the background 179 | // dispatcher after some time has passed. 180 | func DelayedHandlePlaylistSong(f File) error { 181 | rootPoint := f.mPoint 182 | if rootPoint[len(rootPoint)-1] != '/' { 183 | rootPoint = rootPoint + "/" 184 | } 185 | 186 | path := rootPoint + "playlists/" + f.album + "/" + f.name 187 | 188 | extension := filepath.Ext(f.name) 189 | if extension != ".mp3" { 190 | os.Remove(path) 191 | return errors.New("File is not an mp3.") 192 | } 193 | 194 | src, err := os.Stat(path) 195 | if err != nil || src.IsDir() { 196 | return errors.New("File not found.") 197 | } 198 | 199 | err, tags := musicmgr.GetMp3Tags(path) 200 | if err != nil { 201 | os.Remove(path) 202 | return err 203 | } 204 | 205 | artist := store.GetCompatibleString(tags.Artist) 206 | album := store.GetCompatibleString(tags.Album) 207 | title := tags.Title 208 | if strings.HasSuffix(title, ".mp3") { 209 | title = title[:len(title)-len(".mp3")] 210 | } 211 | title = store.GetCompatibleString(title) + ".mp3" 212 | 213 | newPath, err := store.GetFilePath(artist, album, title) 214 | if err == nil { 215 | var playlistFile playlistmgr.PlaylistFile 216 | playlistFile.Title = title 217 | playlistFile.Artist = artist 218 | playlistFile.Album = album 219 | playlistFile.Path = newPath 220 | err = store.AddFileToPlaylist(playlistFile, f.album) 221 | } else { 222 | err = store.HandleDrop(path, rootPoint) 223 | if err == nil { 224 | newPath, err = store.GetFilePath(artist, album, title) 225 | if err == nil { 226 | var playlistFile playlistmgr.PlaylistFile 227 | playlistFile.Title = title 228 | playlistFile.Artist = artist 229 | playlistFile.Album = album 230 | playlistFile.Path = newPath 231 | err = store.AddFileToPlaylist(playlistFile, f.album) 232 | } 233 | } 234 | } 235 | 236 | os.Remove(path) 237 | if err == nil { 238 | err = store.RegeneratePlaylistFile(f.album, rootPoint) 239 | } 240 | return err 241 | } 242 | 243 | // DelayedHandleDrop handles a dropped file 244 | // but is called by the background dispatcher 245 | // after some time has passed. 246 | func DelayedHandleDrop(f File) error { 247 | // Get the dropped file path. 248 | rootPoint := f.mPoint 249 | if rootPoint[len(rootPoint)-1] != '/' { 250 | rootPoint = rootPoint + "/" 251 | } 252 | 253 | path := rootPoint + "drop/" + f.name 254 | err := store.HandleDrop(path, rootPoint) 255 | fmt.Printf("DelayedHandleDrop: %s\n", path) 256 | if err != nil { 257 | glog.Error(err) 258 | return err 259 | } 260 | return nil 261 | } 262 | 263 | var _ fs.HandleReleaser = (*FileHandle)(nil) 264 | 265 | func (fh *FileHandle) Release(ctx context.Context, req *fuse.ReleaseRequest) error { 266 | if fh.r == nil { 267 | if fh.f.name == ".description" { 268 | glog.Infof("Entered Release: .description file\n") 269 | return nil 270 | } 271 | 272 | if fh.f.name[0] == '.' { 273 | return fuse.EPERM 274 | } 275 | } 276 | 277 | if fh.r == nil { 278 | glog.Info("Release: There is no file handler.\n") 279 | return fuse.EIO 280 | } 281 | glog.Infof("Releasing the file: %s\n", fh.r.Name()) 282 | 283 | if fh.f != nil && fh.f.artist == "drop" { 284 | glog.Infof("Entered Release dropping the song: %s\n", fh.f.name) 285 | ret_val := fh.r.Close() 286 | 287 | PushFileItem(*fh.f, DelayedHandleDrop) 288 | return ret_val 289 | } 290 | 291 | if fh.f != nil && fh.f.artist == "playlists" { 292 | glog.Infof("Entered Release with playlist song: %s\n", fh.f.name) 293 | ret_val := fh.r.Close() 294 | 295 | PushFileItem(*fh.f, DelayedHandlePlaylistSong) 296 | return ret_val 297 | } 298 | 299 | // This is not an music file or this is a strange situation. 300 | if fh.f == nil || len(fh.f.artist) < 1 || len(fh.f.album) < 1 { 301 | glog.Info("Entered Release: Artist or Album not set.\n") 302 | return fh.r.Close() 303 | } 304 | 305 | glog.Infof("Entered Release: Artist: %s, Album: %s, Song: %s\n", fh.f.artist, fh.f.album, fh.f.name) 306 | ret_val := fh.r.Close() 307 | extension := filepath.Ext(fh.f.name) 308 | songPath, err := store.GetFilePath(fh.f.artist, fh.f.album, fh.f.name) 309 | if err != nil { 310 | return err 311 | } 312 | 313 | if extension == ".mp3" { 314 | //TODO: Use the correct artist and album 315 | musicmgr.SetMp3Tags(fh.f.artist, fh.f.album, fh.f.song, songPath) 316 | } 317 | return ret_val 318 | } 319 | 320 | var _ = fs.HandleReader(&FileHandle{}) 321 | 322 | func (fh *FileHandle) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { 323 | glog.Infof("Entered Read.\n") 324 | //TODO: Check if we need to add something here for playlists and drop directories. 325 | if fh.r == nil { 326 | if fh.f.name == ".description" { 327 | glog.Info("Reading description file\n") 328 | if len(fh.f.artist) < 1 { 329 | return fuse.ENOENT 330 | } 331 | _, err := store.GetArtistPath(fh.f.artist) 332 | if err != nil { 333 | return err 334 | } 335 | 336 | if len(fh.f.album) > 1 { 337 | _, err = store.GetAlbumPath(fh.f.artist, fh.f.album) 338 | if err != nil { 339 | return err 340 | } 341 | } 342 | descBytes, err := store.GetDescription(fh.f.artist, fh.f.album, fh.f.name) 343 | if err != nil { 344 | return err 345 | } 346 | resp.Data = []byte(descBytes) 347 | return nil 348 | } 349 | 350 | glog.Info("There is no file handler.\n") 351 | return fuse.EIO 352 | } 353 | 354 | glog.Infof("Reading file: %s.\n", fh.r.Name()) 355 | if _, err := fh.r.Seek(req.Offset, 0); err != nil { 356 | return err 357 | } 358 | buf := make([]byte, req.Size) 359 | n, err := fh.r.Read(buf) 360 | resp.Data = buf[:n] 361 | if err != nil && err != io.EOF { 362 | glog.Error(err) 363 | return err 364 | } 365 | return nil 366 | } 367 | 368 | var _ = fs.HandleWriter(&FileHandle{}) 369 | 370 | func (fh *FileHandle) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error { 371 | glog.Infof("Entered Write\n") 372 | //TODO: Check if we need to add something here for playlists and drop directories. 373 | if fh.r == nil { 374 | if fh.f.name == ".description" { 375 | glog.Errorf("Not allowed to write description file.\n") 376 | //TODO: Allow to write description 377 | return nil 378 | } 379 | return fuse.EIO 380 | } 381 | 382 | glog.Infof("Writing file: %s.\n", fh.r.Name()) 383 | if _, err := fh.r.Seek(req.Offset, 0); err != nil { 384 | return err 385 | } 386 | n, err := fh.r.Write(req.Data) 387 | resp.Size = n 388 | return err 389 | } 390 | 391 | var _ = fs.HandleFlusher(&FileHandle{}) 392 | 393 | func (fh *FileHandle) Flush(ctx context.Context, req *fuse.FlushRequest) error { 394 | if fh.f != nil { 395 | glog.Infof("Entered Flush with Song: %s, Artist: %s and Album: %s\n", fh.f.name, fh.f.artist, fh.f.album) 396 | } 397 | 398 | if fh.r == nil { 399 | glog.Infof("There is no file handler.\n") 400 | return fuse.EIO 401 | } 402 | 403 | glog.Infof("Entered Flush with path: %s\n", fh.r.Name()) 404 | 405 | fh.r.Sync() 406 | return nil 407 | } 408 | 409 | var _ = fs.NodeSetattrer(&File{}) 410 | 411 | func (f *File) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error { 412 | glog.Infof("Entered SetAttr with Song: %s, Artist: %s and Album: %s\n", f.name, f.artist, f.album) 413 | 414 | if req.Valid.Size() { 415 | glog.Infof("New size: %d\n", int(req.Size)) 416 | } 417 | return nil 418 | } 419 | -------------------------------------------------------------------------------- /store/moveManager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | // Package store is package for managing the database that stores 18 | // the information about the songs, artists and albums. 19 | package store 20 | 21 | import ( 22 | "encoding/json" 23 | "errors" 24 | "github.com/dankomiocevic/mulifs/musicmgr" 25 | "github.com/dankomiocevic/mulifs/playlistmgr" 26 | "os" 27 | "path/filepath" 28 | 29 | "bazil.org/fuse" 30 | "github.com/boltdb/bolt" 31 | "github.com/golang/glog" 32 | ) 33 | 34 | // MoveSongs changes the Songs path. 35 | // It modifies the information in the database 36 | // and updates the tags to match the new location. 37 | // It also moves the actual file into the new 38 | // location. 39 | func MoveSongs(oldArtist, oldAlbum, oldName, newArtist, newAlbum, newName, path, mPoint string) (string, error) { 40 | glog.Infof("Moving song from Artist: %s, Album: %s, name: %s and path: %s to Artist: %s, Album: %s, name: %s\n", oldArtist, oldAlbum, oldName, path, newArtist, newAlbum, newName) 41 | 42 | // Check file extension. 43 | extension := filepath.Ext(path) 44 | if extension != ".mp3" { 45 | glog.Info("Wrong file format.") 46 | return "", errors.New("Wrong file format.") 47 | } 48 | rootPoint := mPoint 49 | if rootPoint[len(rootPoint)-1] != '/' { 50 | rootPoint = rootPoint + "/" 51 | } 52 | 53 | newFileName := GetCompatibleString(newName[:len(newName)-len(extension)]) + extension 54 | newPath := rootPoint + newArtist + "/" + newAlbum + "/" 55 | newFullPath := newPath + newFileName 56 | 57 | // Get all the Playlists form the file. 58 | songStore, err := GetSong(oldArtist, oldAlbum, oldName) 59 | if err != nil { 60 | glog.Infof("Cannot get the file from the database: %s\n", err) 61 | } 62 | 63 | // Delete the song from all the playlists 64 | for _, pl := range songStore.Playlists { 65 | DeletePlaylistSong(pl, oldName, true) 66 | } 67 | 68 | // Rename the file 69 | err = os.Rename(path, newFullPath) 70 | if err != nil { 71 | glog.Infof("Cannot rename the file: %s\n", err) 72 | return "", err 73 | } 74 | 75 | // Delete the song from the database 76 | err = DeleteSong(oldArtist, oldAlbum, oldName, mPoint) 77 | if err != nil { 78 | glog.Infof("Cannot delete song: %s\n", err) 79 | return "", err 80 | } 81 | 82 | // Change the tags in the file. 83 | musicmgr.SetMp3Tags(newArtist, newAlbum, newName, newFullPath) 84 | // Add the song again to the database. 85 | _, err = CreateSong(newArtist, newAlbum, newName, newPath) 86 | if err != nil { 87 | glog.Infof("Cannot create song in the db: %s\n", err) 88 | return "", err 89 | } 90 | 91 | // Add the song to all the playlists. 92 | for _, pl := range songStore.Playlists { 93 | file := playlistmgr.PlaylistFile{ 94 | Title: newName, 95 | Artist: newArtist, 96 | Album: newAlbum, 97 | Path: newPath, 98 | } 99 | 100 | AddFileToPlaylist(file, pl) 101 | RegeneratePlaylistFile(pl, mPoint) 102 | } 103 | 104 | return newFileName, nil 105 | } 106 | 107 | // processNewArtist returns all the albums inside an Artist and 108 | // prepares the new folder for the new Artist. 109 | // It also creates the description files and Buckets in the DB. 110 | func processNewArtist(newArtist, oldArtist string) ([]string, error) { 111 | var albums []string 112 | 113 | db, err := bolt.Open(config.DbPath, 0600, nil) 114 | if err != nil { 115 | glog.Error("Error opening the database.") 116 | return nil, err 117 | } 118 | defer db.Close() 119 | 120 | newArtistRaw := newArtist 121 | newArtist = GetCompatibleString(newArtist) 122 | 123 | err = db.Update(func(tx *bolt.Tx) error { 124 | root := tx.Bucket([]byte("Artists")) 125 | 126 | // Get oldArtist Bucket 127 | oldArtistBucket := root.Bucket([]byte(oldArtist)) 128 | if oldArtistBucket == nil { 129 | glog.Info("Source Artist not found.") 130 | return errors.New("Artist not found") 131 | } 132 | 133 | // Get the description file or create it if it does not exist 134 | oldDescription := oldArtistBucket.Get([]byte(".description")) 135 | if oldDescription != nil { 136 | var oldArtistStore ArtistStore 137 | 138 | err := json.Unmarshal(oldDescription, &oldArtistStore) 139 | if err == nil { 140 | albums = make([]string, len(oldArtistStore.ArtistAlbums)) 141 | copy(albums, oldArtistStore.ArtistAlbums) 142 | } else { 143 | albums = make([]string, 0) 144 | } 145 | } 146 | 147 | // Create the bucket 148 | artistBucket, err := root.CreateBucketIfNotExists([]byte(newArtist)) 149 | if err != nil { 150 | glog.Info("Cannot create Artist bucket.") 151 | return fuse.EIO 152 | } 153 | 154 | // Get the description file or create it if it does not exist 155 | description := artistBucket.Get([]byte(".description")) 156 | var artistStore ArtistStore 157 | if description == nil { 158 | artistStore = ArtistStore{ 159 | ArtistName: newArtistRaw, 160 | ArtistPath: newArtist, 161 | ArtistAlbums: albums, 162 | } 163 | } else { 164 | err := json.Unmarshal(description, &artistStore) 165 | if err != nil { 166 | return fuse.EIO 167 | } 168 | copy(artistStore.ArtistAlbums, albums) 169 | } 170 | 171 | encoded, err := json.Marshal(artistStore) 172 | if err != nil { 173 | glog.Info("Cannot encode description JSON.") 174 | return fuse.EIO 175 | } 176 | artistBucket.Put([]byte(".description"), encoded) 177 | 178 | return nil 179 | }) 180 | return albums, err 181 | } 182 | 183 | // processNewAlbum returns all the songs inside an Album and 184 | // prepares the new folder for the new Album. 185 | // It also creates the description files and Buckets in the DB. 186 | func processNewAlbum(newArtist, newAlbum, oldArtist, oldAlbum string) ([][]byte, error) { 187 | var songs [][]byte 188 | 189 | db, err := bolt.Open(config.DbPath, 0600, nil) 190 | if err != nil { 191 | glog.Error("Error opening the database.") 192 | return nil, err 193 | } 194 | defer db.Close() 195 | 196 | newAlbumRaw := newAlbum 197 | newAlbum = GetCompatibleString(newAlbum) 198 | 199 | err = db.Update(func(tx *bolt.Tx) error { 200 | root := tx.Bucket([]byte("Artists")) 201 | artistBucket := root.Bucket([]byte(newArtist)) 202 | if artistBucket == nil { 203 | glog.Info("Destination Artist not found.") 204 | return errors.New("Artist not found.") 205 | } 206 | 207 | // Create the bucket 208 | albumBucket, err := artistBucket.CreateBucketIfNotExists([]byte(newAlbum)) 209 | if err != nil { 210 | glog.Info("Cannot create Album bucket.") 211 | return fuse.EIO 212 | } 213 | 214 | // Get the description file or create it if it does not exist 215 | description := albumBucket.Get([]byte(".description")) 216 | if description == nil { 217 | albumStore := &AlbumStore{ 218 | AlbumName: newAlbumRaw, 219 | AlbumPath: newAlbum, 220 | } 221 | 222 | encoded, err := json.Marshal(albumStore) 223 | if err != nil { 224 | glog.Info("Cannot encode description JSON.") 225 | return fuse.EIO 226 | } 227 | albumBucket.Put([]byte(".description"), encoded) 228 | } 229 | 230 | // Update the Artist description 231 | var artistStore ArtistStore 232 | // Update the description of the Artist 233 | descValue := artistBucket.Get([]byte(".description")) 234 | if descValue == nil { 235 | artistStore.ArtistName = newArtist 236 | artistStore.ArtistPath = newArtist 237 | artistStore.ArtistAlbums = []string{newAlbum} 238 | } else { 239 | err := json.Unmarshal(descValue, &artistStore) 240 | if err != nil { 241 | artistStore.ArtistName = newArtist 242 | artistStore.ArtistPath = newArtist 243 | artistStore.ArtistAlbums = []string{newAlbum} 244 | } 245 | 246 | var found bool = false 247 | for _, a := range artistStore.ArtistAlbums { 248 | if a == newAlbum { 249 | found = true 250 | break 251 | } 252 | } 253 | 254 | if found == false { 255 | artistStore.ArtistAlbums = append(artistStore.ArtistAlbums, newAlbum) 256 | } 257 | } 258 | encoded, err := json.Marshal(artistStore) 259 | if err != nil { 260 | return err 261 | } 262 | artistBucket.Put([]byte(".description"), encoded) 263 | 264 | // Get oldArtist Bucket 265 | oldArtistBucket := root.Bucket([]byte(oldArtist)) 266 | if oldArtistBucket == nil { 267 | glog.Info("Source Artist not found.") 268 | return errors.New("Artist not found") 269 | } 270 | 271 | oldAlbumBucket := oldArtistBucket.Bucket([]byte(oldAlbum)) 272 | if oldAlbumBucket == nil { 273 | glog.Info("Source Album not found.") 274 | return errors.New("Album not found") 275 | } 276 | 277 | // Remove the Album from the Artist description 278 | descValue = oldArtistBucket.Get([]byte(".description")) 279 | if descValue != nil { 280 | err := json.Unmarshal(descValue, &artistStore) 281 | if err == nil { 282 | for i, a := range artistStore.ArtistAlbums { 283 | if a == oldAlbum { 284 | artistStore.ArtistAlbums = append(artistStore.ArtistAlbums[:i], artistStore.ArtistAlbums[i+1:]...) 285 | break 286 | } 287 | } 288 | 289 | encoded, err := json.Marshal(artistStore) 290 | if err != nil { 291 | return err 292 | } 293 | oldArtistBucket.Put([]byte(".description"), encoded) 294 | } 295 | } 296 | // Get all the songs and store it in a temporary slice 297 | c := oldAlbumBucket.Cursor() 298 | for k, v := c.First(); k != nil; k, v = c.Next() { 299 | var temp []byte 300 | if k[0] == '.' { 301 | continue 302 | } 303 | temp = make([]byte, len(v)) 304 | copy(temp, v) 305 | songs = append(songs, temp) 306 | } 307 | return nil 308 | }) 309 | return songs, err 310 | } 311 | 312 | // MoveAlbum changes the album path. 313 | // It modifies the information in the database 314 | // and updates the tags to match the new location 315 | // on every song inside the album. 316 | // It also moves the actual files into the new location. 317 | func MoveAlbum(oldArtist, oldAlbum, newArtist, newAlbum, mPoint string) error { 318 | glog.Infof("Moving Album from Artist: %s, Album: %s to Artist: %s, Album: %s\n", oldArtist, oldAlbum, newArtist, newAlbum) 319 | 320 | // Check that the file is being moved in the same level 321 | // Album -> Album 322 | if len(oldArtist) < 1 || len(newArtist) < 1 { 323 | glog.Info("Cannot change Album to Artist.") 324 | return fuse.EPERM 325 | } 326 | 327 | if len(oldAlbum) < 1 || len(newAlbum) < 1 { 328 | glog.Info("Cannot change Album to Artist.") 329 | return fuse.EPERM 330 | } 331 | 332 | rootPoint := mPoint 333 | if rootPoint[len(rootPoint)-1] != '/' { 334 | rootPoint = rootPoint + "/" 335 | } 336 | newPath := rootPoint + newArtist + "/" + newAlbum + "/" 337 | 338 | // Create the directory if not exists 339 | src, err := os.Stat(newPath) 340 | if err != nil || !src.IsDir() { 341 | err := os.Mkdir(newPath, 0777) 342 | if err != nil { 343 | glog.Infof("Cannot create the new directory: %s.", err) 344 | return fuse.EIO 345 | } 346 | } 347 | 348 | var songs [][]byte 349 | songs, err = processNewAlbum(newArtist, newAlbum, oldArtist, oldAlbum) 350 | if err != nil { 351 | return err 352 | } 353 | 354 | glog.Infof("Moving %d songs.\n", len(songs)) 355 | // Move all the songs inside the Album 356 | for _, element := range songs { 357 | var song SongStore 358 | err := json.Unmarshal(element, &song) 359 | if err != nil { 360 | glog.Info("Cannot unmarshall JSON") 361 | continue 362 | } 363 | MoveSongs(oldArtist, oldAlbum, song.SongPath, newArtist, newAlbum, song.SongPath, song.SongFullPath, mPoint) 364 | } 365 | 366 | db, err := bolt.Open(config.DbPath, 0600, nil) 367 | if err != nil { 368 | glog.Error("Error opening the database.") 369 | return err 370 | } 371 | defer db.Close() 372 | 373 | // Finally delete the old Artist bucket 374 | err = db.Update(func(tx *bolt.Tx) error { 375 | root := tx.Bucket([]byte("Artists")) 376 | artistBucket := root.Bucket([]byte(oldArtist)) 377 | if artistBucket == nil { 378 | return errors.New("Artist not found.") 379 | } 380 | err = artistBucket.DeleteBucket([]byte(oldAlbum)) 381 | return err 382 | }) 383 | 384 | if err != nil { 385 | return fuse.EIO 386 | } 387 | return nil 388 | } 389 | 390 | // MoveArtist changes the Artist path. 391 | // It modifies the information in the database 392 | // and updates the tags to match the new location 393 | // on every song inside every album. 394 | // It also moves the actual files into the new location. 395 | func MoveArtist(oldArtist, newArtist, mPoint string) error { 396 | glog.Infof("Moving Artist from: %s to %s\n", oldArtist, newArtist) 397 | 398 | // Check that all the information is ready 399 | if len(oldArtist) < 1 || len(newArtist) < 1 { 400 | return fuse.EIO 401 | } 402 | 403 | rootPoint := mPoint 404 | if rootPoint[len(rootPoint)-1] != '/' { 405 | rootPoint = rootPoint + "/" 406 | } 407 | newPath := rootPoint + newArtist + "/" 408 | 409 | // Create the directory if not exists 410 | src, err := os.Stat(newPath) 411 | if err != nil || !src.IsDir() { 412 | err := os.Mkdir(newPath, 0777) 413 | if err != nil { 414 | glog.Infof("Cannot create the new directory: %s.", err) 415 | return fuse.EIO 416 | } 417 | } 418 | 419 | var albums []string 420 | albums, err = processNewArtist(newArtist, oldArtist) 421 | if err != nil { 422 | return err 423 | } 424 | 425 | glog.Infof("Moving %d albums.\n", len(albums)) 426 | // Move all the songs inside the Album 427 | for _, element := range albums { 428 | MoveAlbum(oldArtist, element, newArtist, element, mPoint) 429 | } 430 | 431 | db, err := bolt.Open(config.DbPath, 0600, nil) 432 | if err != nil { 433 | glog.Error("Error opening the database.") 434 | return err 435 | } 436 | defer db.Close() 437 | 438 | // Finally delete the old Album bucket 439 | err = db.Update(func(tx *bolt.Tx) error { 440 | root := tx.Bucket([]byte("Artists")) 441 | err = root.DeleteBucket([]byte(oldArtist)) 442 | return err 443 | }) 444 | 445 | if err != nil { 446 | return fuse.EIO 447 | } 448 | return nil 449 | } 450 | -------------------------------------------------------------------------------- /dir.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/dankomiocevic/mulifs/store" 21 | "io/ioutil" 22 | "os" 23 | "path/filepath" 24 | "runtime" 25 | 26 | "bazil.org/fuse" 27 | "bazil.org/fuse/fs" 28 | "github.com/golang/glog" 29 | "golang.org/x/net/context" 30 | ) 31 | 32 | // Dir struct specifies a Directory in the 33 | // filesystem, the Directories can be Artist or Albums. 34 | // The root Directory that contains all the Artists 35 | // is also a Directory. 36 | type Dir struct { 37 | fs *FS 38 | artist string 39 | album string 40 | mPoint string 41 | } 42 | 43 | var _ = fs.Node(&Dir{}) 44 | 45 | func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) error { 46 | glog.Infof("Entered Attr dir: Artist: %s, Album: %s\n", d.artist, d.album) 47 | a.Mode = os.ModeDir | 0777 48 | if config_params.uid != 0 { 49 | a.Uid = uint32(config_params.uid) 50 | } 51 | if config_params.gid != 0 { 52 | a.Gid = uint32(config_params.gid) 53 | } 54 | a.Size = 4096 55 | return nil 56 | } 57 | 58 | var dirDirs = []fuse.Dirent{ 59 | {Name: "drop", Type: fuse.DT_Dir}, 60 | {Name: "playlists", Type: fuse.DT_Dir}, 61 | } 62 | 63 | var _ = fs.NodeStringLookuper(&Dir{}) 64 | 65 | func (d *Dir) Lookup(ctx context.Context, name string) (fs.Node, error) { 66 | glog.Infof("Entering Lookup with artist: %s, album: %s and name: %s.\n", d.artist, d.album, name) 67 | if name == ".description" { 68 | return &File{artist: d.artist, album: d.album, song: name, name: name, mPoint: d.mPoint}, nil 69 | } 70 | 71 | if name[0] == '.' { 72 | return nil, fuse.EIO 73 | } 74 | 75 | if len(d.artist) < 1 { 76 | if name == "drop" { 77 | return &Dir{fs: d.fs, artist: "drop", album: "", mPoint: d.mPoint}, nil 78 | } 79 | if name == "playlists" { 80 | return &Dir{fs: d.fs, artist: "playlists", album: "", mPoint: d.mPoint}, nil 81 | } 82 | 83 | _, err := store.GetArtistPath(name) 84 | if err != nil { 85 | glog.Info(err) 86 | return nil, err 87 | } 88 | return &Dir{fs: d.fs, artist: name, album: "", mPoint: d.mPoint}, nil 89 | } 90 | 91 | if len(d.album) < 1 && d.artist != "drop" && d.artist != "playlists" { 92 | _, err := store.GetAlbumPath(d.artist, name) 93 | if err != nil { 94 | glog.Info(err) 95 | return nil, err 96 | } 97 | return &Dir{fs: d.fs, artist: d.artist, album: name, mPoint: d.mPoint}, nil 98 | } 99 | 100 | var err error 101 | if d.artist == "drop" { 102 | _, err = store.GetDropFilePath(name, d.mPoint) 103 | if err != nil { 104 | glog.Info(err) 105 | return nil, fuse.ENOENT 106 | } 107 | } else if d.artist == "playlists" { 108 | if len(d.album) < 1 { 109 | _, err = store.GetPlaylistPath(name) 110 | if err != nil { 111 | glog.Info(err) 112 | return nil, fuse.ENOENT 113 | } 114 | return &Dir{fs: d.fs, artist: d.artist, album: name, mPoint: d.mPoint}, nil 115 | } else { 116 | _, err = store.GetPlaylistFilePath(d.album, name, d.mPoint) 117 | if err != nil { 118 | glog.Info(err) 119 | return nil, fuse.ENOENT 120 | } 121 | } 122 | } else { 123 | _, err = store.GetFilePath(d.artist, d.album, name) 124 | if err != nil { 125 | glog.Info(err) 126 | return nil, err 127 | } 128 | } 129 | extension := filepath.Ext(name) 130 | songName := name[:len(name)-len(extension)] 131 | return &File{artist: d.artist, album: d.album, song: songName, name: name, mPoint: d.mPoint}, nil 132 | } 133 | 134 | var _ = fs.HandleReadDirAller(&Dir{}) 135 | 136 | func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 137 | glog.Infof("Entering ReadDirAll\n") 138 | if len(d.artist) < 1 { 139 | a, err := store.ListArtists() 140 | if err != nil { 141 | return nil, fuse.ENOENT 142 | } 143 | for _, v := range dirDirs { 144 | a = append(a, v) 145 | } 146 | return a, nil 147 | } 148 | 149 | if d.artist == "drop" { 150 | if len(d.album) > 0 { 151 | return nil, fuse.ENOENT 152 | } 153 | 154 | rootPoint := d.mPoint 155 | if rootPoint[len(rootPoint)-1] != '/' { 156 | rootPoint = rootPoint + "/" 157 | } 158 | 159 | path := rootPoint + "drop" 160 | // Check if the drop directory exists 161 | src, err := os.Stat(path) 162 | if err != nil { 163 | return nil, nil 164 | } 165 | 166 | // Check if it is a directory 167 | if !src.IsDir() { 168 | return nil, nil 169 | } 170 | 171 | var a []fuse.Dirent 172 | files, _ := ioutil.ReadDir(path) 173 | for _, f := range files { 174 | var node fuse.Dirent 175 | node.Name = f.Name() 176 | node.Type = fuse.DT_File 177 | a = append(a, node) 178 | } 179 | return a, nil 180 | } 181 | 182 | if d.artist == "playlists" { 183 | if len(d.album) < 1 { 184 | a, err := store.ListPlaylists() 185 | if err != nil { 186 | return nil, fuse.ENOENT 187 | } 188 | return a, nil 189 | } 190 | 191 | a, err := store.ListPlaylistSongs(d.album, d.mPoint) 192 | if err != nil { 193 | return nil, fuse.ENOENT 194 | } 195 | 196 | return a, nil 197 | } 198 | 199 | if len(d.album) < 1 { 200 | a, err := store.ListAlbums(d.artist) 201 | if err != nil { 202 | return nil, fuse.ENOENT 203 | } 204 | return a, nil 205 | } 206 | 207 | a, err := store.ListSongs(d.artist, d.album) 208 | if err != nil { 209 | return nil, fuse.ENOENT 210 | } 211 | 212 | return a, nil 213 | } 214 | 215 | var _ = fs.NodeMkdirer(&Dir{}) 216 | 217 | func (d *Dir) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fs.Node, error) { 218 | name := req.Name 219 | glog.Infof("Entering mkdir with name: %s.\n", name) 220 | // Do not allow creating directories starting with dot 221 | if name[0] == '.' { 222 | glog.Info("Names starting with dot are not allowed.") 223 | return nil, fuse.EPERM 224 | } 225 | 226 | if d.mPoint[len(d.mPoint)-1] != '/' { 227 | d.mPoint = d.mPoint + "/" 228 | } 229 | if len(d.artist) < 1 { 230 | glog.Info("Creating an Artist.") 231 | ret, err := store.CreateArtist(name) 232 | if err != nil { 233 | glog.Infof("Error creating artist: %s\n", err) 234 | return nil, err 235 | } 236 | 237 | path := d.mPoint + ret 238 | err = os.MkdirAll(path, 0777) 239 | if err != nil { 240 | glog.Infof("Error creating artist folder: %s\n", err) 241 | return nil, fuse.EIO 242 | } 243 | return &Dir{fs: d.fs, artist: ret, album: "", mPoint: d.mPoint}, nil 244 | } 245 | 246 | if d.artist == "drop" { 247 | return nil, fuse.EIO 248 | } 249 | 250 | if d.artist == "playlists" { 251 | if len(d.album) < 1 { 252 | ret, err := store.CreatePlaylist(name, d.mPoint) 253 | if err != nil { 254 | glog.Infof("Error creating playlist: %s\n", err) 255 | return nil, err 256 | } 257 | 258 | err = store.RegeneratePlaylistFile(ret, d.mPoint) 259 | if err != nil { 260 | glog.Infof("Error regenerating playlist: %s\n", err) 261 | return nil, err 262 | } 263 | return &Dir{fs: d.fs, artist: "playlists", album: ret, mPoint: d.mPoint}, nil 264 | } 265 | return nil, fuse.EPERM 266 | } 267 | 268 | if len(d.album) < 1 { 269 | glog.Infof("Creating album: %s in artist: %s.\n", d.artist, name) 270 | ret, err := store.CreateAlbum(d.artist, name) 271 | if err != nil { 272 | return nil, err 273 | } 274 | path := d.mPoint + d.artist + "/" + ret 275 | err = os.MkdirAll(path, 0777) 276 | if err != nil { 277 | glog.Infof("Error creating artist folder: %s\n", err) 278 | return nil, fuse.EIO 279 | } 280 | return &Dir{fs: d.fs, artist: d.artist, album: ret, mPoint: d.mPoint}, nil 281 | } 282 | 283 | return nil, fuse.EIO 284 | } 285 | 286 | var _ = fs.NodeCreater(&Dir{}) 287 | 288 | func (d *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) { 289 | glog.Infof("Entered Create Dir\n") 290 | 291 | if req.Flags.IsReadOnly() { 292 | glog.Info("Create: File requested is read only.\n") 293 | } 294 | if req.Flags.IsReadWrite() { 295 | glog.Info("Create: File requested is read write.\n") 296 | } 297 | if req.Flags.IsWriteOnly() { 298 | glog.Info("Create: File requested is write only.\n") 299 | } 300 | 301 | if runtime.GOOS == "darwin" { 302 | resp.Flags |= fuse.OpenDirectIO 303 | } 304 | 305 | if d.artist == "drop" { 306 | if len(d.album) > 0 { 307 | glog.Info("Subdirectories are not allowed in drop folder.") 308 | return nil, nil, fuse.EIO 309 | } 310 | 311 | rootPoint := d.mPoint 312 | if rootPoint[len(rootPoint)-1] != '/' { 313 | rootPoint = rootPoint + "/" 314 | } 315 | 316 | name := req.Name 317 | path := rootPoint + "drop/" 318 | extension := filepath.Ext(name) 319 | 320 | if extension != ".mp3" { 321 | glog.Info("Only mp3 files are allowed.") 322 | return nil, nil, fuse.EIO 323 | } 324 | 325 | // Check if the drop directory exists 326 | src, err := os.Stat(path) 327 | if err != nil || !src.IsDir() { 328 | err = os.MkdirAll(path, 0777) 329 | if err != nil { 330 | glog.Infof("Cannot create dir: %s\n", err) 331 | return nil, nil, err 332 | } 333 | } 334 | 335 | fi, err := os.Create(path + name) 336 | if err != nil { 337 | glog.Infof("Cannot create file: %s\n", err) 338 | return nil, nil, err 339 | } 340 | 341 | keyName := name[:len(name)-len(extension)] 342 | f := &File{ 343 | artist: d.artist, 344 | album: d.album, 345 | song: keyName, 346 | name: name, 347 | mPoint: d.mPoint, 348 | } 349 | 350 | if fi != nil { 351 | glog.Infof("Returning file handle for: %s.\n", fi.Name()) 352 | } 353 | return f, &FileHandle{r: fi, f: f}, nil 354 | } 355 | 356 | if d.artist == "playlists" { 357 | if len(d.album) < 1 { 358 | glog.Info("Files are not allowed outside playlists.") 359 | return nil, nil, fuse.EIO 360 | } 361 | 362 | rootPoint := d.mPoint 363 | if rootPoint[len(rootPoint)-1] != '/' { 364 | rootPoint = rootPoint + "/" 365 | } 366 | 367 | name := req.Name 368 | path := rootPoint + "playlists/" + d.album 369 | extension := filepath.Ext(name) 370 | 371 | if extension != ".mp3" { 372 | glog.Info("Only mp3 files are allowed.") 373 | return nil, nil, fuse.EIO 374 | } 375 | 376 | // Check if the playlist drop directory exists 377 | src, err := os.Stat(path) 378 | if err != nil || !src.IsDir() { 379 | err = os.MkdirAll(path, 0777) 380 | if err != nil { 381 | glog.Infof("Cannot create dir: %s\n", err) 382 | return nil, nil, err 383 | } 384 | } 385 | 386 | fi, err := os.Create(path + "/" + name) 387 | if err != nil { 388 | glog.Infof("Cannot create file: %s\n", err) 389 | return nil, nil, err 390 | } 391 | 392 | keyName := name[:len(name)-len(extension)] 393 | f := &File{ 394 | artist: d.artist, 395 | album: d.album, 396 | song: keyName, 397 | name: name, 398 | mPoint: d.mPoint, 399 | } 400 | 401 | if fi != nil { 402 | glog.Infof("Returning file handle for: %s.\n", fi.Name()) 403 | } 404 | return f, &FileHandle{r: fi, f: f}, nil 405 | } 406 | 407 | if len(d.artist) < 1 || len(d.album) < 1 { 408 | return nil, nil, fuse.EPERM 409 | } 410 | 411 | nameRaw := req.Name 412 | if nameRaw[0] == '.' { 413 | glog.Info("Cannot create files starting with dot.") 414 | return nil, nil, fuse.EPERM 415 | } 416 | 417 | rootPoint := d.mPoint 418 | if rootPoint[len(rootPoint)-1] != '/' { 419 | rootPoint = rootPoint + "/" 420 | } 421 | 422 | path := rootPoint + d.artist + "/" + d.album + "/" 423 | name, err := store.CreateSong(d.artist, d.album, nameRaw, path) 424 | if err != nil { 425 | glog.Info("Error creating song.") 426 | return nil, nil, fuse.EPERM 427 | } 428 | 429 | err = os.MkdirAll(path, 0777) 430 | if err != nil { 431 | glog.Info("Cannot create folder.") 432 | return nil, nil, err 433 | } 434 | 435 | fi, err := os.Create(path + name) 436 | if err != nil { 437 | glog.Infof("Cannot create file: %s\n", err) 438 | return nil, nil, err 439 | } 440 | 441 | extension := filepath.Ext(name) 442 | keyName := name[:len(name)-len(extension)] 443 | f := &File{ 444 | artist: d.artist, 445 | album: d.album, 446 | song: keyName, 447 | name: name, 448 | mPoint: d.mPoint, 449 | } 450 | 451 | if fi != nil { 452 | glog.Infof("Returning file handle for: %s.\n", fi.Name()) 453 | } 454 | return f, &FileHandle{r: fi, f: f}, nil 455 | } 456 | 457 | var _ = fs.NodeRemover(&Dir{}) 458 | 459 | func (d *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) error { 460 | //TODO: Correct this function to work with drop folder. 461 | name := req.Name 462 | glog.Infof("Entered Remove function with Artist: %s, Album: %s and Name: %s.\n", d.artist, d.album, name) 463 | 464 | if name == ".description" { 465 | return nil 466 | } 467 | 468 | if name[0] == '.' { 469 | return fuse.EIO 470 | } 471 | 472 | if req.Dir { 473 | if len(name) < 1 { 474 | return fuse.EIO 475 | } 476 | 477 | if len(d.artist) < 1 { 478 | if name == "drop" { 479 | return fuse.EIO 480 | } 481 | 482 | if name == "playlists" { 483 | return fuse.EIO 484 | } 485 | 486 | err := store.DeleteArtist(name, d.mPoint) 487 | if err != nil { 488 | return fuse.EIO 489 | } 490 | 491 | return nil 492 | } 493 | 494 | if d.artist == "playlists" { 495 | store.DeletePlaylist(name, d.mPoint) 496 | return nil 497 | } 498 | 499 | err := store.DeleteAlbum(d.artist, name, d.mPoint) 500 | if err != nil { 501 | return fuse.EIO 502 | } 503 | 504 | return nil 505 | } else { 506 | if len(d.artist) < 1 || len(d.album) < 1 { 507 | return fuse.EIO 508 | } 509 | 510 | fullPath, err := store.GetFilePath(d.artist, d.album, name) 511 | if err != nil { 512 | return fuse.EIO 513 | } 514 | 515 | if d.artist == "playlists" { 516 | err := store.DeletePlaylistSong(d.album, name, false) 517 | if err != nil { 518 | return fuse.EIO 519 | } 520 | } 521 | 522 | err = store.DeleteSong(d.artist, d.album, name, d.mPoint) 523 | if err != nil { 524 | return fuse.EIO 525 | } 526 | 527 | //TODO: Check if there are no more files in the folder 528 | // and delete the folder. 529 | 530 | err = os.Remove(fullPath) 531 | if err != nil { 532 | return err 533 | } 534 | 535 | return nil 536 | } 537 | } 538 | 539 | var _ = fs.NodeRenamer(&Dir{}) 540 | 541 | func (d *Dir) Rename(ctx context.Context, r *fuse.RenameRequest, newDir fs.Node) error { 542 | var newD *Dir 543 | 544 | newD = newDir.(*Dir) 545 | glog.Infof("Renaming: OldName: %s, NewName: %s, newDir: %s/%s\n", r.OldName, r.NewName, newD.artist, newD.album) 546 | 547 | if d.mPoint[len(d.mPoint)-1] != '/' { 548 | d.mPoint = d.mPoint + "/" 549 | } 550 | path := d.mPoint + d.artist + "/" + d.album + "/" + r.OldName 551 | 552 | if r.OldName == ".description" || r.NewName == ".description" { 553 | return fuse.EPERM 554 | } 555 | 556 | if r.NewName[0] == '.' || r.OldName[0] == '.' { 557 | glog.Info("Names starting with dot are not allowed.") 558 | return fuse.EPERM 559 | } 560 | 561 | if len(d.artist) < 1 { 562 | glog.Info("Changing artist name.") 563 | if len(newD.artist) > 0 { 564 | return fuse.EPERM 565 | } 566 | 567 | err := store.MoveArtist(r.OldName, r.NewName, d.mPoint) 568 | return err 569 | } 570 | 571 | if d.artist == "drop" { 572 | glog.Info("Cannot rename inside drop folder.") 573 | return fuse.EPERM 574 | } 575 | 576 | if d.artist == "playlists" { 577 | glog.Info("Rename inside playlists folder.") 578 | var err error 579 | if len(d.album) < 1 { 580 | glog.Info("Rename playlist name.") 581 | var newName string 582 | if len(newD.album) < 1 { 583 | newName, err = store.RenamePlaylist(r.OldName, r.NewName, d.mPoint) 584 | } else { 585 | newName, err = store.RenamePlaylist(r.OldName, newD.album, d.mPoint) 586 | } 587 | if err != nil { 588 | return fuse.EIO 589 | } 590 | err = store.RegeneratePlaylistFile(newName, d.mPoint) 591 | if err != nil { 592 | return fuse.EIO 593 | } 594 | return nil 595 | } 596 | 597 | _, err = store.RenamePlaylistSong(d.album, r.OldName, r.NewName, d.mPoint) 598 | if err != nil { 599 | return fuse.EIO 600 | } 601 | 602 | err = store.RegeneratePlaylistFile(d.album, d.mPoint) 603 | if err != nil { 604 | return fuse.EIO 605 | } 606 | 607 | return nil 608 | } 609 | 610 | if len(d.album) < 1 { 611 | glog.Info("Moving album") 612 | if len(newD.album) > 0 { 613 | return fuse.EPERM 614 | } 615 | 616 | err := store.MoveAlbum(d.artist, r.OldName, newD.artist, r.NewName, d.mPoint) 617 | return err 618 | } 619 | 620 | _, err := store.MoveSongs(d.artist, d.album, r.OldName, newD.artist, newD.album, r.NewName, path, d.mPoint) 621 | if err != nil { 622 | return fuse.EIO 623 | } 624 | return nil 625 | } 626 | -------------------------------------------------------------------------------- /store/playlistManager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | package store 18 | 19 | import ( 20 | "bazil.org/fuse" 21 | "encoding/json" 22 | "errors" 23 | "github.com/boltdb/bolt" 24 | "github.com/dankomiocevic/mulifs/playlistmgr" 25 | "github.com/golang/glog" 26 | "io/ioutil" 27 | "os" 28 | ) 29 | 30 | // GetPlaylistPath checks that a specified playlist 31 | // exists on the database and returns an 32 | // error if it does not. 33 | // It also returns the playlist name as string. 34 | func GetPlaylistPath(playlist string) (string, error) { 35 | glog.Infof("Entered Playlist path with playlist: %s\n", playlist) 36 | db, err := bolt.Open(config.DbPath, 0600, nil) 37 | if err != nil { 38 | return "", err 39 | } 40 | defer db.Close() 41 | 42 | err = db.View(func(tx *bolt.Tx) error { 43 | root := tx.Bucket([]byte("Playlists")) 44 | if root == nil { 45 | return errors.New("No playlists.") 46 | } 47 | 48 | playlistBucket := root.Bucket([]byte(playlist)) 49 | if playlistBucket == nil { 50 | return errors.New("Playlist not exists.") 51 | } 52 | 53 | return nil 54 | }) 55 | 56 | return playlist, err 57 | } 58 | 59 | // GetPlaylistFilePath function should return the path for a specific 60 | // file in a specific playlist. 61 | // The file could be on two places, first option is that the file is 62 | // stored in the database. In that case, the file will be stored somewhere 63 | // else in the MuLi filesystem but that will be specified on the 64 | // item in the database. 65 | // On the other hand, the file could be just dropped inside the playlist 66 | // and it will be temporary stored in a directory inside the playlists 67 | // directory. 68 | // The playlist name is specified on the first argument and the song 69 | // name on the second. 70 | // The mount path is also needed and should be specified on the third 71 | // argument. 72 | // This function returns a string containing the file path and an error 73 | // that will be nil if everything is ok. 74 | func GetPlaylistFilePath(playlist, song, mPoint string) (string, error) { 75 | glog.Infof("Entered Playlist file path with song: %s, and playlist: %s\n", song, playlist) 76 | 77 | returnValue, err := getPlaylistFile(playlist, song) 78 | if err == nil { 79 | return returnValue.Path, nil 80 | } 81 | 82 | if mPoint[len(mPoint)-1] != '/' { 83 | mPoint = mPoint + "/" 84 | } 85 | 86 | fullPath := mPoint + "playlists/" + playlist + "/" + song 87 | // Check if the file exists 88 | src, err := os.Stat(fullPath) 89 | if err != nil || src.IsDir() { 90 | return "", errors.New("File not exists.") 91 | } 92 | 93 | return fullPath, nil 94 | } 95 | 96 | // ListPlaylists function returns all the names of the playlists available 97 | // in the MuLi system. 98 | // It receives no arguments and returns a slice of Dir objects to list 99 | // all the available playlists and the error if there is any. 100 | func ListPlaylists() ([]fuse.Dirent, error) { 101 | glog.Info("Entered list playlists.") 102 | db, err := bolt.Open(config.DbPath, 0600, nil) 103 | if err != nil { 104 | return nil, err 105 | } 106 | defer db.Close() 107 | 108 | var a []fuse.Dirent 109 | err = db.View(func(tx *bolt.Tx) error { 110 | b := tx.Bucket([]byte("Playlists")) 111 | if b == nil { 112 | glog.Infof("There is no Playlists bucket.") 113 | return nil 114 | } 115 | c := b.Cursor() 116 | for k, v := c.First(); k != nil; k, v = c.Next() { 117 | if v == nil { 118 | var node fuse.Dirent 119 | node.Name = string(k) 120 | node.Type = fuse.DT_Dir 121 | a = append(a, node) 122 | } 123 | } 124 | return nil 125 | }) 126 | return a, nil 127 | } 128 | 129 | // ListPlaylistSongs function returns all the songs inside a playlist. 130 | // The available songs are loaded from the database and also from the 131 | // temporary drop directory named after the playlist. 132 | // It receives a playlist name and returns a slice with all the 133 | // files. 134 | func ListPlaylistSongs(playlist, mPoint string) ([]fuse.Dirent, error) { 135 | glog.Infof("Listing contents of playlist %s.\n", playlist) 136 | db, err := bolt.Open(config.DbPath, 0600, nil) 137 | if err != nil { 138 | return nil, err 139 | } 140 | defer db.Close() 141 | 142 | var a []fuse.Dirent 143 | err = db.View(func(tx *bolt.Tx) error { 144 | root := tx.Bucket([]byte("Playlists")) 145 | if root == nil { 146 | return nil 147 | } 148 | 149 | b := root.Bucket([]byte(playlist)) 150 | if b == nil { 151 | return nil 152 | } 153 | 154 | c := b.Cursor() 155 | for k, v := c.First(); k != nil; k, v = c.Next() { 156 | if v != nil { 157 | var node fuse.Dirent 158 | node.Name = string(k) 159 | node.Type = fuse.DT_File 160 | a = append(a, node) 161 | } 162 | } 163 | return nil 164 | }) 165 | 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | if mPoint[len(mPoint)-1] != '/' { 171 | mPoint = mPoint + "/" 172 | } 173 | 174 | fullPath := mPoint + "playlists/" + playlist + "/" 175 | 176 | files, _ := ioutil.ReadDir(fullPath) 177 | for _, f := range files { 178 | if !f.IsDir() { 179 | var node fuse.Dirent 180 | node.Name = string(f.Name()) 181 | node.Type = fuse.DT_File 182 | a = append(a, node) 183 | } 184 | } 185 | return a, nil 186 | } 187 | 188 | // CreatePlaylist function creates a playlist item in the database and 189 | // also creates it in the filesystem. 190 | // It receives the playlist name and returns the modified name and an 191 | // error if something went wrong. 192 | func CreatePlaylist(name, mPoint string) (string, error) { 193 | glog.Infof("Creating Playlist with name: %s\n", name) 194 | name = GetCompatibleString(name) 195 | db, err := bolt.Open(config.DbPath, 0600, nil) 196 | if err != nil { 197 | return "", err 198 | } 199 | defer db.Close() 200 | 201 | err = db.Update(func(tx *bolt.Tx) error { 202 | root, err := tx.CreateBucketIfNotExists([]byte("Playlists")) 203 | if err != nil { 204 | glog.Errorf("Error creating Playlists bucket: %s\n", err) 205 | return err 206 | } 207 | 208 | _, err = root.CreateBucketIfNotExists([]byte(name)) 209 | if err != nil { 210 | glog.Errorf("Error creating %s bucket: %s\n", name, err) 211 | return err 212 | } 213 | 214 | return nil 215 | }) 216 | if err != nil { 217 | return "", err 218 | } 219 | 220 | return name, err 221 | } 222 | 223 | // RegeneratePlaylistFile creates the playlist file from the 224 | // information in the database. 225 | func RegeneratePlaylistFile(name, mPoint string) error { 226 | glog.Infof("Regenerating playlist for name: %s\n", name) 227 | db, err := bolt.Open(config.DbPath, 0600, nil) 228 | if err != nil { 229 | return err 230 | } 231 | defer db.Close() 232 | 233 | var a []playlistmgr.PlaylistFile 234 | err = db.View(func(tx *bolt.Tx) error { 235 | root := tx.Bucket([]byte("Playlists")) 236 | if root == nil { 237 | glog.Info("Cannot open Playlists bucket.") 238 | return errors.New("Cannot open Playlists bucket.") 239 | } 240 | 241 | b := root.Bucket([]byte(name)) 242 | if b == nil { 243 | glog.Infof("Playlist %s not exists", name) 244 | return errors.New("Playlist not exists.") 245 | } 246 | 247 | c := b.Cursor() 248 | for k, v := c.First(); k != nil; k, v = c.Next() { 249 | if v != nil { 250 | var file playlistmgr.PlaylistFile 251 | err := json.Unmarshal(v, &file) 252 | if err == nil { 253 | a = append(a, file) 254 | } else { 255 | glog.Errorf("Cannot unmarshal Playlist File %s: %s\n", k, err) 256 | } 257 | } 258 | } 259 | return nil 260 | }) 261 | 262 | if err != nil { 263 | return err 264 | } 265 | 266 | return playlistmgr.RegeneratePlaylistFile(a, name, mPoint) 267 | } 268 | 269 | // AddFileToPlaylist function adds a file to a specific playlist. 270 | // The function also checks that the file exists in the MuLi database. 271 | func AddFileToPlaylist(file playlistmgr.PlaylistFile, playlistName string) error { 272 | path, err := GetFilePath(file.Artist, file.Album, file.Title) 273 | if err != nil { 274 | return errors.New("Playlist item not found in MuLi.") 275 | } 276 | 277 | file.Path = path 278 | db, err := bolt.Open(config.DbPath, 0600, nil) 279 | if err != nil { 280 | return err 281 | } 282 | defer db.Close() 283 | 284 | err = db.Update(func(tx *bolt.Tx) error { 285 | root := tx.Bucket([]byte("Playlists")) 286 | if root == nil { 287 | glog.Errorf("Error opening Playlists bucket: %s\n", err) 288 | return errors.New("Error opening Playlists bucket.") 289 | } 290 | 291 | playlistBucket := root.Bucket([]byte(playlistName)) 292 | if playlistBucket == nil { 293 | glog.Errorf("Error opening %s playlist bucket: %s\n", playlistName, err) 294 | return errors.New("Error opening playlist bucket.") 295 | } 296 | 297 | encoded, err := json.Marshal(file) 298 | if err != nil { 299 | glog.Errorf("Cannot encode PlaylistFile.") 300 | return err 301 | } 302 | playlistBucket.Put([]byte(file.Title), encoded) 303 | 304 | // Update the original file playlists to have a link to the 305 | // current playlist. 306 | root = tx.Bucket([]byte("Artists")) 307 | if root == nil { 308 | glog.Errorf("Error opening Artists bucket: %s\n", err) 309 | return errors.New("Error opening Artists bucket.") 310 | } 311 | 312 | artistBucket := root.Bucket([]byte(file.Artist)) 313 | if artistBucket == nil { 314 | glog.Errorf("Error opening %s artist bucket: %s\n", file.Artist, err) 315 | return errors.New("Error opening artist bucket.") 316 | } 317 | 318 | albumBucket := artistBucket.Bucket([]byte(file.Album)) 319 | if albumBucket == nil { 320 | glog.Errorf("Error opening %s album on %s artist: %s\n", file.Album, file.Artist, err) 321 | return errors.New("Error opening album bucket.") 322 | } 323 | 324 | songJson := albumBucket.Get([]byte(file.Title)) 325 | 326 | if songJson == nil { 327 | glog.Errorf("Error opening %s on %s album on %s artist: %s\n", file.Title, file.Album, file.Artist, err) 328 | return errors.New("Error opening song json.") 329 | } 330 | 331 | var song SongStore 332 | err = json.Unmarshal(songJson, &song) 333 | if err != nil { 334 | return err 335 | } 336 | 337 | if song.Playlists != nil { 338 | for _, list := range song.Playlists { 339 | if list == playlistName { 340 | return nil 341 | } 342 | } 343 | } 344 | 345 | song.Playlists = append(song.Playlists, playlistName) 346 | encoded, err = json.Marshal(song) 347 | if err != nil { 348 | return err 349 | } 350 | 351 | return albumBucket.Put([]byte(file.Title), encoded) 352 | }) 353 | 354 | return err 355 | } 356 | 357 | // DeletePlaylist function deletes a playlist from the database 358 | // and also deletes all the entries in the specific files and 359 | // deletes it from the filesystem. 360 | func DeletePlaylist(name, mPoint string) error { 361 | db, err := bolt.Open(config.DbPath, 0600, nil) 362 | if err != nil { 363 | return err 364 | } 365 | defer db.Close() 366 | 367 | err = db.Update(func(tx *bolt.Tx) error { 368 | root := tx.Bucket([]byte("Playlists")) 369 | if root == nil { 370 | glog.Errorf("Error opening Playlists bucket.\n") 371 | return errors.New("Error opening Playlists bucket.") 372 | } 373 | 374 | playlistBucket := root.Bucket([]byte(name)) 375 | if playlistBucket == nil { 376 | return nil 377 | } 378 | 379 | c := playlistBucket.Cursor() 380 | for k, songJson := c.First(); k != nil; k, songJson = c.Next() { 381 | if songJson == nil { 382 | glog.Infof("Error opening song Json: %s in playlist: %s\n", k, name) 383 | continue 384 | } 385 | 386 | // Get the PlaylistFile 387 | var file playlistmgr.PlaylistFile 388 | err = json.Unmarshal(songJson, &file) 389 | if err != nil { 390 | continue 391 | } 392 | 393 | // Open the song in the MuLi database 394 | // to remove the playlists connection. 395 | artistsBucket := tx.Bucket([]byte("Artists")) 396 | if artistsBucket == nil { 397 | glog.Error("Cannot open Artists bucket.") 398 | return errors.New("Cannot open Artists bucket.") 399 | } 400 | 401 | artistBucket := artistsBucket.Bucket([]byte(file.Artist)) 402 | if artistBucket == nil { 403 | glog.Infof("Cannot open Artist bucket: %s.\n", file.Artist) 404 | continue 405 | } 406 | 407 | albumBucket := artistBucket.Bucket([]byte(file.Album)) 408 | if albumBucket == nil { 409 | glog.Infof("Cannot open Album bucket: %s in Artist: %s.\n", file.Album, file.Artist) 410 | continue 411 | } 412 | 413 | jsonFile := albumBucket.Get([]byte(file.Title)) 414 | if jsonFile == nil { 415 | glog.Infof("Cannot open song: %s Album bucket: %s in Artist: %s.\n", file.Title, file.Album, file.Artist) 416 | continue 417 | } 418 | 419 | var song SongStore 420 | err = json.Unmarshal(jsonFile, &song) 421 | if err != nil { 422 | return err 423 | } 424 | 425 | // Remove the playlist from the song's playlists list 426 | if song.Playlists != nil { 427 | for i, list := range song.Playlists { 428 | if list == name { 429 | song.Playlists = append(song.Playlists[:i], song.Playlists[i+1:]...) 430 | break 431 | } 432 | } 433 | } 434 | 435 | encoded, err := json.Marshal(song) 436 | if err != nil { 437 | return err 438 | } 439 | 440 | // Store the modified song version 441 | return albumBucket.Put([]byte(k), encoded) 442 | 443 | } 444 | 445 | return root.DeleteBucket([]byte(name)) 446 | }) 447 | 448 | return playlistmgr.DeletePlaylist(name, mPoint) 449 | } 450 | 451 | // DeletePlaylistSong function deletes a specific song from a playlist. 452 | // The force parameter is used to just delete the song without modifying 453 | // the original song file. 454 | func DeletePlaylistSong(playlist, name string, force bool) error { 455 | db, err := bolt.Open(config.DbPath, 0600, nil) 456 | if err != nil { 457 | return err 458 | } 459 | defer db.Close() 460 | 461 | err = db.Update(func(tx *bolt.Tx) error { 462 | root := tx.Bucket([]byte("Playlists")) 463 | if root == nil { 464 | glog.Errorf("Error opening Playlists bucket.\n") 465 | return errors.New("Error opening Playlists bucket.") 466 | } 467 | 468 | playlistBucket := root.Bucket([]byte(playlist)) 469 | if playlistBucket == nil { 470 | glog.Infof("Cannot open Playlist bucket: %s\n", playlist) 471 | return errors.New("Error opening Playlist bucket.") 472 | } 473 | 474 | if force == false { 475 | songJson := playlistBucket.Get([]byte(name)) 476 | if songJson == nil { 477 | return nil 478 | } 479 | 480 | // Get the playlist file 481 | var file playlistmgr.PlaylistFile 482 | err = json.Unmarshal(songJson, &file) 483 | if err != nil { 484 | return nil 485 | } 486 | 487 | // Open the song in the MuLi database 488 | // to remove the playlists connection. 489 | artistsBucket := tx.Bucket([]byte("Artists")) 490 | if artistsBucket == nil { 491 | glog.Error("Cannot open Artists bucket.") 492 | return errors.New("Cannot open Artists bucket.") 493 | } 494 | 495 | artistBucket := artistsBucket.Bucket([]byte(file.Artist)) 496 | if artistBucket == nil { 497 | glog.Infof("Cannot open Artist bucket: %s.\n", file.Artist) 498 | return nil 499 | } 500 | 501 | albumBucket := artistBucket.Bucket([]byte(file.Album)) 502 | if albumBucket == nil { 503 | glog.Infof("Cannot open Album bucket: %s in Artist: %s.\n", file.Album, file.Artist) 504 | return nil 505 | } 506 | 507 | jsonFile := albumBucket.Get([]byte(file.Title)) 508 | if jsonFile == nil { 509 | glog.Infof("Cannot open song: %s Album bucket: %s in Artist: %s.\n", file.Title, file.Album, file.Artist) 510 | return nil 511 | } 512 | 513 | var song SongStore 514 | err = json.Unmarshal(jsonFile, &song) 515 | if err != nil { 516 | return err 517 | } 518 | 519 | // Remove the playlist from the song's playlists list 520 | if song.Playlists != nil { 521 | for i, list := range song.Playlists { 522 | if list == name { 523 | song.Playlists = append(song.Playlists[:i], song.Playlists[i+1:]...) 524 | break 525 | } 526 | } 527 | } 528 | 529 | encoded, err := json.Marshal(song) 530 | if err != nil { 531 | return err 532 | } 533 | 534 | // Store the modified song version 535 | return albumBucket.Put([]byte(name), encoded) 536 | } 537 | 538 | return playlistBucket.Delete([]byte(name)) 539 | }) 540 | return err 541 | } 542 | 543 | // getPlaylistFile returns a PlaylistFile struct 544 | // with all the information from a specific file 545 | // inside a playlist. 546 | func getPlaylistFile(playlist, song string) (playlistmgr.PlaylistFile, error) { 547 | glog.Infof("Entered getPlaylistFile with song: %s, and playlist: %s\n", song, playlist) 548 | db, err := bolt.Open(config.DbPath, 0600, nil) 549 | if err != nil { 550 | return playlistmgr.PlaylistFile{}, err 551 | } 552 | defer db.Close() 553 | 554 | var returnValue playlistmgr.PlaylistFile 555 | err = db.View(func(tx *bolt.Tx) error { 556 | root := tx.Bucket([]byte("Playlists")) 557 | if root == nil { 558 | return errors.New("No playlists.") 559 | } 560 | playlistBucket := root.Bucket([]byte(playlist)) 561 | if playlistBucket == nil { 562 | return errors.New("Playlist not exists.") 563 | } 564 | 565 | songJson := playlistBucket.Get([]byte(song)) 566 | if songJson == nil { 567 | return errors.New("Song not found.") 568 | } 569 | 570 | err := json.Unmarshal(songJson, &returnValue) 571 | if err != nil { 572 | return errors.New("Cannot open song.") 573 | } 574 | return nil 575 | }) 576 | 577 | if err == nil { 578 | return returnValue, nil 579 | } 580 | return playlistmgr.PlaylistFile{}, err 581 | } 582 | 583 | // RenamePlaylist moves the entire Playlist and changes all 584 | // the links to the songs in every MuLi song. 585 | func RenamePlaylist(oldName, newName, mPoint string) (string, error) { 586 | glog.Infof("Renaming %s playlist to %s.\n", oldName, newName) 587 | newName = GetCompatibleString(newName) 588 | db, err := bolt.Open(config.DbPath, 0600, nil) 589 | if err != nil { 590 | return "", err 591 | } 592 | defer db.Close() 593 | 594 | err = db.Update(func(tx *bolt.Tx) error { 595 | root := tx.Bucket([]byte("Playlists")) 596 | if root == nil { 597 | glog.Errorf("Error opening Playlists bucket.\n") 598 | return errors.New("Error opening Playlists bucket.") 599 | } 600 | 601 | playlistBucket := root.Bucket([]byte(oldName)) 602 | if playlistBucket == nil { 603 | return nil 604 | } 605 | 606 | newPlaylistBucket, err := root.CreateBucketIfNotExists([]byte(newName)) 607 | if err != nil { 608 | glog.Infof("Cannot create new playlist bucket: %s\n", err) 609 | return err 610 | } 611 | 612 | c := playlistBucket.Cursor() 613 | for k, songJson := c.First(); k != nil; k, songJson = c.Next() { 614 | if songJson == nil { 615 | glog.Infof("Error opening song Json: %s in playlist: %s\n", k, oldName) 616 | continue 617 | } 618 | 619 | // Get the PlaylistFile 620 | var file playlistmgr.PlaylistFile 621 | err = json.Unmarshal(songJson, &file) 622 | if err != nil { 623 | continue 624 | } 625 | 626 | // Open the song in the MuLi database 627 | // to remove the playlists connection. 628 | artistsBucket := tx.Bucket([]byte("Artists")) 629 | if artistsBucket == nil { 630 | glog.Error("Cannot open Artists bucket.") 631 | return errors.New("Cannot open Artists bucket.") 632 | } 633 | 634 | artistBucket := artistsBucket.Bucket([]byte(file.Artist)) 635 | if artistBucket == nil { 636 | glog.Infof("Cannot open Artist bucket: %s.\n", file.Artist) 637 | continue 638 | } 639 | 640 | albumBucket := artistBucket.Bucket([]byte(file.Album)) 641 | if albumBucket == nil { 642 | glog.Infof("Cannot open Album bucket: %s in Artist: %s.\n", file.Album, file.Artist) 643 | continue 644 | } 645 | 646 | jsonFile := albumBucket.Get([]byte(file.Title)) 647 | if jsonFile == nil { 648 | glog.Infof("Cannot open song: %s Album bucket: %s in Artist: %s.\n", file.Title, file.Album, file.Artist) 649 | continue 650 | } 651 | 652 | var song SongStore 653 | err = json.Unmarshal(jsonFile, &song) 654 | if err != nil { 655 | return err 656 | } 657 | 658 | // Remove the playlist from the song's playlists list 659 | if song.Playlists != nil { 660 | for i, list := range song.Playlists { 661 | if list == oldName { 662 | song.Playlists[i] = newName 663 | break 664 | } 665 | } 666 | } 667 | 668 | encoded, err := json.Marshal(song) 669 | if err != nil { 670 | return err 671 | } 672 | 673 | // Store the modified song version 674 | err = albumBucket.Put([]byte(k), encoded) 675 | if err != nil { 676 | continue 677 | } 678 | 679 | err = newPlaylistBucket.Put(k, songJson) 680 | if err != nil { 681 | continue 682 | } 683 | } 684 | 685 | playlistmgr.DeletePlaylist(oldName, mPoint) 686 | return root.DeleteBucket([]byte(oldName)) 687 | }) 688 | 689 | if err != nil { 690 | return "", err 691 | } 692 | 693 | return newName, nil 694 | } 695 | 696 | // RenamePlaylistSong changes the name on a specific song, 697 | // it also updates the song in the original place and 698 | // checks that every playlist containing the song is updated. 699 | func RenamePlaylistSong(playlist, oldName, newName, mPoint string) (string, error) { 700 | file, err := getPlaylistFile(playlist, oldName) 701 | if err != nil { 702 | glog.Infof("Cannot open playlist file: %s\n", err) 703 | return "", err 704 | } 705 | 706 | newName, err = MoveSongs(file.Artist, file.Album, file.Title, file.Artist, file.Album, newName, file.Path, mPoint) 707 | return newName, err 708 | } 709 | -------------------------------------------------------------------------------- /store/objectmanager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Author: Danko Miocevic 16 | 17 | // Package store is package for managing the database that stores 18 | // the information about the songs, artists and albums. 19 | package store 20 | 21 | import ( 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "github.com/dankomiocevic/mulifs/musicmgr" 26 | "os" 27 | "path/filepath" 28 | "regexp" 29 | "strings" 30 | "unicode" 31 | 32 | "bazil.org/fuse" 33 | "github.com/boltdb/bolt" 34 | "github.com/golang/glog" 35 | "golang.org/x/text/transform" 36 | "golang.org/x/text/unicode/norm" 37 | ) 38 | 39 | // config stores the general configuration for the store. 40 | // DbPath is the path to the database file. 41 | var config struct { 42 | DbPath string 43 | } 44 | 45 | // ArtistStore is the information for a specific artist 46 | // to be stored in the database. 47 | type ArtistStore struct { 48 | ArtistName string 49 | ArtistPath string 50 | ArtistAlbums []string 51 | } 52 | 53 | // AlbumStore is the information for a specific album 54 | // to be stored in the database. 55 | type AlbumStore struct { 56 | AlbumName string 57 | AlbumPath string 58 | } 59 | 60 | // SongStore is the information for a specific song 61 | // to be stored in the database. 62 | type SongStore struct { 63 | SongName string 64 | SongPath string 65 | SongFullPath string 66 | Playlists []string 67 | } 68 | 69 | // InitDB initializes the database with the 70 | // specified configuration and returns nil if 71 | // there was no problem. 72 | func InitDB(path string) error { 73 | db, err := bolt.Open(path, 0600, nil) 74 | if err != nil { 75 | return err 76 | } 77 | defer db.Close() 78 | 79 | err = db.Update(func(tx *bolt.Tx) error { 80 | _, err = tx.CreateBucketIfNotExists([]byte("Artists")) 81 | if err != nil { 82 | glog.Errorf("Error creating bucket: %s", err) 83 | return fmt.Errorf("Error creating bucket: %s", err) 84 | } 85 | return nil 86 | }) 87 | 88 | if err != nil { 89 | return err 90 | } 91 | 92 | config.DbPath = path 93 | return nil 94 | } 95 | 96 | // isMn checks if the rune is in the Unicode 97 | // category Mn. 98 | func isMn(r rune) bool { 99 | return unicode.Is(unicode.Mn, r) 100 | } 101 | 102 | // GetCompatibleString removes all the special characters 103 | // from the string name to create a new string compatible 104 | // with different file names. 105 | func GetCompatibleString(name string) string { 106 | // Replace all the & signs with and text 107 | name = strings.Replace(name, "&", "and", -1) 108 | // Change all the characters to ASCII 109 | t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) 110 | result, _, _ := transform.String(t, name) 111 | // Replace all the spaces with underscore 112 | s, _ := regexp.Compile(`\s+`) 113 | result = s.ReplaceAllString(result, "_") 114 | // Remove all the non alphanumeric characters 115 | r, _ := regexp.Compile(`\W`) 116 | result = r.ReplaceAllString(result, "") 117 | return result 118 | } 119 | 120 | // StoreNewSong takes the information received from 121 | // the song file tags and creates the item in the 122 | // database accordingly. It checks the different fields 123 | // and completes the missing information with the default 124 | // data. 125 | func StoreNewSong(song *musicmgr.FileTags, path string) error { 126 | db, err := bolt.Open(config.DbPath, 0600, nil) 127 | if err != nil { 128 | return err 129 | } 130 | defer db.Close() 131 | 132 | var artistStore ArtistStore 133 | var albumStore AlbumStore 134 | var songStore SongStore 135 | 136 | err = db.Update(func(tx *bolt.Tx) error { 137 | // Get the artists bucket 138 | artistsBucket, updateError := tx.CreateBucketIfNotExists([]byte("Artists")) 139 | if updateError != nil { 140 | glog.Errorf("Error creating bucket: %s", updateError) 141 | return fmt.Errorf("Error creating bucket: %s", updateError) 142 | } 143 | 144 | // Generate the compatible names for the fields 145 | artistPath := GetCompatibleString(song.Artist) 146 | albumPath := GetCompatibleString(song.Album) 147 | songPath := GetCompatibleString(song.Title) 148 | 149 | // Generate artist bucket 150 | artistBucket, updateError := artistsBucket.CreateBucketIfNotExists([]byte(artistPath)) 151 | if updateError != nil { 152 | glog.Errorf("Error creating bucket: %s", updateError) 153 | return fmt.Errorf("Error creating bucket: %s", updateError) 154 | } 155 | 156 | // Update the description of the Artist 157 | descValue := artistBucket.Get([]byte(".description")) 158 | if descValue == nil { 159 | artistStore.ArtistName = song.Artist 160 | artistStore.ArtistPath = artistPath 161 | artistStore.ArtistAlbums = []string{albumPath} 162 | } else { 163 | err := json.Unmarshal(descValue, &artistStore) 164 | if err != nil { 165 | artistStore.ArtistName = song.Artist 166 | artistStore.ArtistPath = artistPath 167 | artistStore.ArtistAlbums = []string{albumPath} 168 | } 169 | 170 | var found bool = false 171 | for _, a := range artistStore.ArtistAlbums { 172 | if a == albumPath { 173 | found = true 174 | break 175 | } 176 | } 177 | 178 | if found == false { 179 | artistStore.ArtistAlbums = append(artistStore.ArtistAlbums, albumPath) 180 | } 181 | } 182 | encoded, err := json.Marshal(artistStore) 183 | if err != nil { 184 | return err 185 | } 186 | artistBucket.Put([]byte(".description"), encoded) 187 | 188 | // Get the album bucket 189 | albumBucket, updateError := artistBucket.CreateBucketIfNotExists([]byte(albumPath)) 190 | if updateError != nil { 191 | glog.Errorf("Error creating bucket: %s", updateError) 192 | return fmt.Errorf("Error creating bucket: %s", updateError) 193 | } 194 | 195 | // Update the album description 196 | albumStore.AlbumName = song.Album 197 | albumStore.AlbumPath = albumPath 198 | encoded, err = json.Marshal(albumStore) 199 | if err != nil { 200 | return err 201 | } 202 | albumBucket.Put([]byte(".description"), encoded) 203 | 204 | _, file := filepath.Split(path) 205 | extension := filepath.Ext(file) 206 | 207 | // Add the song to the album bucket 208 | songStore.SongName = song.Title 209 | songStore.SongPath = songPath + extension 210 | songStore.SongFullPath = path 211 | 212 | encoded, err = json.Marshal(songStore) 213 | if err != nil { 214 | return err 215 | } 216 | 217 | albumBucket.Put([]byte(songPath+extension), encoded) 218 | return nil 219 | }) 220 | 221 | return nil 222 | } 223 | 224 | // ListArtists returns all the Dirent corresponding 225 | // to Artists in the database. 226 | // This is used to generate the Artist listing on the 227 | // generated filesystem. 228 | // It returns nil in the second return value if there 229 | // was no error and nil if the Artists were 230 | // obtained correctly. 231 | func ListArtists() ([]fuse.Dirent, error) { 232 | db, err := bolt.Open(config.DbPath, 0600, nil) 233 | if err != nil { 234 | return nil, err 235 | } 236 | defer db.Close() 237 | 238 | var a []fuse.Dirent 239 | err = db.View(func(tx *bolt.Tx) error { 240 | b := tx.Bucket([]byte("Artists")) 241 | c := b.Cursor() 242 | for k, v := c.First(); k != nil; k, v = c.Next() { 243 | if v == nil { 244 | var node fuse.Dirent 245 | node.Name = string(k) 246 | node.Type = fuse.DT_Dir 247 | a = append(a, node) 248 | } else { 249 | var node fuse.Dirent 250 | node.Name = string(k) 251 | node.Type = fuse.DT_File 252 | a = append(a, node) 253 | } 254 | } 255 | return nil 256 | }) 257 | 258 | if err != nil { 259 | return nil, err 260 | } 261 | 262 | return a, nil 263 | } 264 | 265 | // ListAlbums returns all the Dirent corresponding 266 | // to Albums for a specified Artist in the database. 267 | // This is used to generate the Album listing on the 268 | // generated filesystem. 269 | // It returns nil in the second return value if there 270 | // was no error and nil if the Albums were 271 | // obtained correctly. 272 | func ListAlbums(artist string) ([]fuse.Dirent, error) { 273 | db, err := bolt.Open(config.DbPath, 0600, nil) 274 | if err != nil { 275 | return nil, err 276 | } 277 | defer db.Close() 278 | 279 | var a []fuse.Dirent 280 | err = db.View(func(tx *bolt.Tx) error { 281 | root := tx.Bucket([]byte("Artists")) 282 | b := root.Bucket([]byte(artist)) 283 | c := b.Cursor() 284 | for k, v := c.First(); k != nil; k, v = c.Next() { 285 | if v == nil { 286 | var node fuse.Dirent 287 | node.Name = string(k) 288 | node.Type = fuse.DT_Dir 289 | a = append(a, node) 290 | } else { 291 | var node fuse.Dirent 292 | node.Name = string(k) 293 | node.Type = fuse.DT_File 294 | a = append(a, node) 295 | } 296 | } 297 | return nil 298 | }) 299 | 300 | if err != nil { 301 | return nil, err 302 | } 303 | 304 | return a, nil 305 | } 306 | 307 | // ListSongs returns all the Dirent corresponding 308 | // to Songs for a specified Artist and Album 309 | // in the database. 310 | // This is used to generate the Song listing on the 311 | // generated filesystem. 312 | // It returns nil in the second return value if there 313 | // was no error and nil if the Songs were 314 | // obtained correctly. 315 | func ListSongs(artist string, album string) ([]fuse.Dirent, error) { 316 | db, err := bolt.Open(config.DbPath, 0600, nil) 317 | if err != nil { 318 | return nil, err 319 | } 320 | defer db.Close() 321 | 322 | var a []fuse.Dirent 323 | err = db.View(func(tx *bolt.Tx) error { 324 | root := tx.Bucket([]byte("Artists")) 325 | artistBucket := root.Bucket([]byte(artist)) 326 | b := artistBucket.Bucket([]byte(album)) 327 | c := b.Cursor() 328 | for k, v := c.First(); k != nil; k, v = c.Next() { 329 | var song SongStore 330 | if k[0] != '.' || string(k) == ".description" { 331 | err := json.Unmarshal(v, &song) 332 | if err != nil { 333 | continue 334 | } 335 | } 336 | var node fuse.Dirent 337 | node.Name = string(k) 338 | node.Type = fuse.DT_File 339 | a = append(a, node) 340 | } 341 | return nil 342 | }) 343 | 344 | if err != nil { 345 | return nil, err 346 | } 347 | 348 | return a, nil 349 | } 350 | 351 | // GetArtistPath checks that a specified Artist 352 | // exists on the database and returns a fuse 353 | // error if it does not. 354 | // It also returns the Artist name as string. 355 | func GetArtistPath(artist string) (string, error) { 356 | db, err := bolt.Open(config.DbPath, 0600, nil) 357 | if err != nil { 358 | return "", err 359 | } 360 | defer db.Close() 361 | 362 | err = db.View(func(tx *bolt.Tx) error { 363 | root := tx.Bucket([]byte("Artists")) 364 | artistBucket := root.Bucket([]byte(artist)) 365 | 366 | if artistBucket == nil { 367 | return fuse.ENOENT 368 | } 369 | return nil 370 | }) 371 | 372 | if err != nil { 373 | return "", err 374 | } 375 | 376 | return artist, nil 377 | } 378 | 379 | // GetAlbumPath checks that a specified Artist 380 | // and Album exists on the database and returns 381 | // a fuse error if it does not. 382 | // It also returns the Album name as string. 383 | func GetAlbumPath(artist string, album string) (string, error) { 384 | db, err := bolt.Open(config.DbPath, 0600, nil) 385 | if err != nil { 386 | return "", err 387 | } 388 | defer db.Close() 389 | 390 | err = db.View(func(tx *bolt.Tx) error { 391 | root := tx.Bucket([]byte("Artists")) 392 | artistBucket := root.Bucket([]byte(artist)) 393 | 394 | if artistBucket == nil { 395 | return fuse.ENOENT 396 | } 397 | 398 | albumBucket := artistBucket.Bucket([]byte(album)) 399 | 400 | if albumBucket == nil { 401 | return fuse.ENOENT 402 | } 403 | 404 | return nil 405 | }) 406 | 407 | if err != nil { 408 | return "", err 409 | } 410 | 411 | return artist, nil 412 | } 413 | 414 | // GetSong returns a SongStore object from the database. 415 | // If there is an error obtaining the Song 416 | // the error will be returned. 417 | func GetSong(artist, album, song string) (SongStore, error) { 418 | glog.Infof("Getting file for song: %s Artist: %s Album: %s\n", song, artist, album) 419 | db, err := bolt.Open(config.DbPath, 0600, nil) 420 | if err != nil { 421 | return SongStore{}, err 422 | } 423 | defer db.Close() 424 | 425 | var returnValue SongStore 426 | err = db.View(func(tx *bolt.Tx) error { 427 | root := tx.Bucket([]byte("Artists")) 428 | if root == nil { 429 | return fuse.EIO 430 | } 431 | 432 | artistBucket := root.Bucket([]byte(artist)) 433 | if artistBucket == nil { 434 | return fuse.ENOENT 435 | } 436 | 437 | albumBucket := artistBucket.Bucket([]byte(album)) 438 | if albumBucket == nil { 439 | return fuse.ENOENT 440 | } 441 | 442 | songJson := albumBucket.Get([]byte(song)) 443 | if songJson == nil { 444 | glog.Info("Song not found.") 445 | return fuse.ENOENT 446 | } 447 | 448 | err := json.Unmarshal(songJson, &returnValue) 449 | if err != nil { 450 | glog.Error("Cannot open song.") 451 | return errors.New("Cannot open song.") 452 | } 453 | return nil 454 | }) 455 | 456 | if err != nil { 457 | return SongStore{}, err 458 | } 459 | return returnValue, nil 460 | } 461 | 462 | // GetFilePath checks that a specified Song 463 | // Album exists on the database and returns 464 | // the full path to the Song file. 465 | // If there is an error obtaining the Song 466 | // an error will be returned. 467 | func GetFilePath(artist, album, song string) (string, error) { 468 | glog.Infof("Getting file path for song: %s Artist: %s Album: %s\n", song, artist, album) 469 | db, err := bolt.Open(config.DbPath, 0600, nil) 470 | if err != nil { 471 | return "", err 472 | } 473 | defer db.Close() 474 | 475 | var returnValue string 476 | 477 | err = db.View(func(tx *bolt.Tx) error { 478 | root := tx.Bucket([]byte("Artists")) 479 | if root == nil { 480 | return fuse.EIO 481 | } 482 | 483 | artistBucket := root.Bucket([]byte(artist)) 484 | if artistBucket == nil { 485 | return fuse.ENOENT 486 | } 487 | 488 | albumBucket := artistBucket.Bucket([]byte(album)) 489 | if albumBucket == nil { 490 | return fuse.ENOENT 491 | } 492 | 493 | songJson := albumBucket.Get([]byte(song)) 494 | if songJson == nil { 495 | glog.Info("Song not found.") 496 | return fuse.ENOENT 497 | } 498 | 499 | var songStore SongStore 500 | err := json.Unmarshal(songJson, &songStore) 501 | if err != nil { 502 | glog.Error("Cannot open song.") 503 | return errors.New("Cannot open song.") 504 | } 505 | returnValue = songStore.SongFullPath 506 | return nil 507 | }) 508 | 509 | if err != nil { 510 | return "", err 511 | } 512 | return returnValue, nil 513 | } 514 | 515 | // GetDescription obtains a Song description from the 516 | // database as a JSON object. 517 | // If the description is obtained correctly a string with 518 | // the JSON is returned and nil. 519 | func GetDescription(artist string, album string, name string) (string, error) { 520 | db, err := bolt.Open(config.DbPath, 0600, nil) 521 | if err != nil { 522 | return "", err 523 | } 524 | defer db.Close() 525 | 526 | var returnValue string 527 | 528 | err = db.View(func(tx *bolt.Tx) error { 529 | root := tx.Bucket([]byte("Artists")) 530 | artistBucket := root.Bucket([]byte(artist)) 531 | var descJson []byte 532 | if len(album) < 1 { 533 | descJson = artistBucket.Get([]byte(name)) 534 | } else { 535 | b := artistBucket.Bucket([]byte(album)) 536 | descJson = b.Get([]byte(name)) 537 | } 538 | 539 | if descJson == nil { 540 | return fuse.ENOENT 541 | } 542 | 543 | returnValue = string(descJson) + "\n" 544 | return nil 545 | }) 546 | 547 | if err != nil { 548 | return "", err 549 | } 550 | return returnValue, nil 551 | } 552 | 553 | // CreateArtist creates a new artist from a Raw 554 | // name. It generates the compatible string to 555 | // use as Directory name and stores the information 556 | // in the database. It also generates the description 557 | // file. 558 | // If there is an error it will be specified in the 559 | // error return value, nil otherwise. 560 | func CreateArtist(nameRaw string) (string, error) { 561 | name := GetCompatibleString(nameRaw) 562 | db, err := bolt.Open(config.DbPath, 0600, nil) 563 | if err != nil { 564 | return name, err 565 | } 566 | defer db.Close() 567 | 568 | err = db.Update(func(tx *bolt.Tx) error { 569 | root := tx.Bucket([]byte("Artists")) 570 | artistBucket, createError := root.CreateBucket([]byte(name)) 571 | if createError != nil { 572 | if createError == bolt.ErrBucketExists { 573 | return fuse.EEXIST 574 | } else { 575 | return fuse.EIO 576 | } 577 | } 578 | 579 | var artistStore ArtistStore 580 | artistStore.ArtistName = nameRaw 581 | artistStore.ArtistPath = name 582 | artistStore.ArtistAlbums = []string{} 583 | 584 | encoded, err := json.Marshal(artistStore) 585 | if err != nil { 586 | return err 587 | } 588 | artistBucket.Put([]byte(".description"), encoded) 589 | return nil 590 | }) 591 | 592 | return name, err 593 | } 594 | 595 | // CreateAlbum creates an album for a specific Artist 596 | // from a Raw name. The name is returned as a compatible 597 | // string to use as a Directory name and the description 598 | // file is created. 599 | // The description file for the Artist will be also updated 600 | // in the process. 601 | // If the Album is created correctly the string return value 602 | // will contain the compatible string to use as Directory 603 | // name and the second value will contain nil. 604 | func CreateAlbum(artist string, nameRaw string) (string, error) { 605 | name := GetCompatibleString(nameRaw) 606 | db, err := bolt.Open(config.DbPath, 0600, nil) 607 | if err != nil { 608 | return name, err 609 | } 610 | defer db.Close() 611 | 612 | err = db.Update(func(tx *bolt.Tx) error { 613 | root := tx.Bucket([]byte("Artists")) 614 | artistBucket := root.Bucket([]byte(artist)) 615 | if artistBucket == nil { 616 | return fuse.ENOENT 617 | } 618 | 619 | var artistStore ArtistStore 620 | // Create the album bucket 621 | albumBucket, createError := artistBucket.CreateBucket([]byte(name)) 622 | if createError != nil { 623 | if createError == bolt.ErrBucketExists { 624 | return fuse.EEXIST 625 | } else { 626 | return fuse.EIO 627 | } 628 | } 629 | 630 | // Update the description of the Artist 631 | descValue := artistBucket.Get([]byte(".description")) 632 | if descValue == nil { 633 | artistStore.ArtistName = artist 634 | artistStore.ArtistPath = artist 635 | artistStore.ArtistAlbums = []string{name} 636 | } else { 637 | err := json.Unmarshal(descValue, &artistStore) 638 | if err != nil { 639 | artistStore.ArtistName = artist 640 | artistStore.ArtistPath = artist 641 | artistStore.ArtistAlbums = []string{name} 642 | } 643 | 644 | var found bool = false 645 | for _, a := range artistStore.ArtistAlbums { 646 | if a == name { 647 | found = true 648 | break 649 | } 650 | } 651 | 652 | if found == false { 653 | artistStore.ArtistAlbums = append(artistStore.ArtistAlbums, name) 654 | } 655 | } 656 | encoded, err := json.Marshal(artistStore) 657 | if err != nil { 658 | return err 659 | } 660 | artistBucket.Put([]byte(".description"), encoded) 661 | 662 | // Update the album description 663 | var albumStore AlbumStore 664 | albumStore.AlbumName = nameRaw 665 | albumStore.AlbumPath = name 666 | encoded, err = json.Marshal(albumStore) 667 | if err != nil { 668 | return err 669 | } 670 | albumBucket.Put([]byte(".description"), encoded) 671 | return nil 672 | }) 673 | 674 | return name, err 675 | } 676 | 677 | // CreateSong creates a song for a specific Artist and Album 678 | // from a Raw name and a path. The name is returned as a compatible 679 | // string to use as a Directory name and the description 680 | // file is created. 681 | // The path parameter is used to identify the file being 682 | // added to the filesystem. 683 | // If the Song is created correctly the string return value 684 | // will contain the compatible string to use as File 685 | // name and the second value will contain nil. 686 | func CreateSong(artist string, album string, nameRaw string, path string) (string, error) { 687 | glog.Infof("Adding song to the DB: %s with Artist: %s and Album: %s\n", nameRaw, artist, album) 688 | extension := filepath.Ext(nameRaw) 689 | if extension != ".mp3" { 690 | return "", errors.New("Wrong file format.") 691 | } 692 | 693 | nameRaw = nameRaw[:len(nameRaw)-len(extension)] 694 | name := GetCompatibleString(nameRaw) 695 | 696 | db, err := bolt.Open(config.DbPath, 0600, nil) 697 | if err != nil { 698 | return name, err 699 | } 700 | defer db.Close() 701 | 702 | err = db.Update(func(tx *bolt.Tx) error { 703 | root := tx.Bucket([]byte("Artists")) 704 | artistBucket := root.Bucket([]byte(artist)) 705 | if artistBucket == nil { 706 | return errors.New("Artist not found.") 707 | } 708 | albumBucket := artistBucket.Bucket([]byte(album)) 709 | if albumBucket == nil { 710 | return errors.New("Album not found.") 711 | } 712 | 713 | var songStore SongStore 714 | songStore.SongName = nameRaw 715 | songStore.SongPath = name + extension 716 | songStore.SongFullPath = path + name + extension 717 | 718 | encoded, err := json.Marshal(songStore) 719 | if err != nil { 720 | return err 721 | } 722 | 723 | albumBucket.Put([]byte(name+extension), encoded) 724 | glog.Infof("Created with name: %s\n", name+extension) 725 | return nil 726 | }) 727 | 728 | return name + extension, err 729 | } 730 | 731 | // DeleteArtist deletes the specified Artist only 732 | // in the database and returns nil if there was no error. 733 | func DeleteArtist(artist, mPoint string) error { 734 | glog.Infof("Deleting Artist: %s\n", artist) 735 | db, err := bolt.Open(config.DbPath, 0600, nil) 736 | if err != nil { 737 | return err 738 | } 739 | defer db.Close() 740 | 741 | var songList []SongStore 742 | err = db.Update(func(tx *bolt.Tx) error { 743 | root := tx.Bucket([]byte("Artists")) 744 | buck := root.Bucket([]byte(artist)) 745 | 746 | c := buck.Cursor() 747 | for k, _ := c.First(); k != nil; k, _ = c.Next() { 748 | if k[0] == '.' { 749 | continue 750 | } 751 | album := buck.Bucket([]byte(k)) 752 | if album == nil { 753 | continue 754 | } 755 | d := album.Cursor() 756 | for name, _ := d.First(); name != nil; name, _ = d.Next() { 757 | if name[0] == '.' { 758 | continue 759 | } 760 | songJson := album.Get([]byte(name)) 761 | if songJson == nil { 762 | continue 763 | } 764 | 765 | var song SongStore 766 | err := json.Unmarshal(songJson, &song) 767 | if err != nil { 768 | continue 769 | } 770 | song.SongName = string(name) 771 | songList = append(songList, song) 772 | } 773 | } 774 | root.DeleteBucket([]byte(artist)) 775 | return nil 776 | }) 777 | 778 | if err != nil { 779 | return err 780 | } 781 | 782 | for _, v := range songList { 783 | if v.Playlists != nil { 784 | for _, list := range v.Playlists { 785 | DeletePlaylistSong(list, v.SongName, true) 786 | RegeneratePlaylistFile(list, mPoint) 787 | } 788 | } 789 | os.Remove(v.SongFullPath) 790 | } 791 | return nil 792 | } 793 | 794 | // DeleteAlbum deletes the specified Album for 795 | // the specified Artist only in the database and 796 | // returns nil if there was no error. 797 | func DeleteAlbum(artistName, albumName, mPoint string) error { 798 | db, err := bolt.Open(config.DbPath, 0600, nil) 799 | if err != nil { 800 | return err 801 | } 802 | defer db.Close() 803 | 804 | var songList []SongStore 805 | err = db.Update(func(tx *bolt.Tx) error { 806 | root := tx.Bucket([]byte("Artists")) 807 | artistBucket := root.Bucket([]byte(artistName)) 808 | if artistBucket == nil { 809 | return errors.New("Artist not found.") 810 | } 811 | 812 | album := artistBucket.Bucket([]byte(artistName)) 813 | if album == nil { 814 | return nil 815 | } 816 | d := album.Cursor() 817 | for name, _ := d.First(); name != nil; name, _ = d.Next() { 818 | if name[0] == '.' { 819 | continue 820 | } 821 | songJson := album.Get([]byte(name)) 822 | if songJson == nil { 823 | continue 824 | } 825 | 826 | var song SongStore 827 | err := json.Unmarshal(songJson, &song) 828 | if err != nil { 829 | continue 830 | } 831 | song.SongName = string(name) 832 | songList = append(songList, song) 833 | } 834 | artistBucket.DeleteBucket([]byte(albumName)) 835 | return nil 836 | }) 837 | 838 | if err != nil { 839 | return err 840 | } 841 | 842 | for _, v := range songList { 843 | if v.Playlists != nil { 844 | for _, list := range v.Playlists { 845 | DeletePlaylistSong(list, v.SongName, true) 846 | RegeneratePlaylistFile(list, mPoint) 847 | } 848 | } 849 | os.Remove(v.SongFullPath) 850 | } 851 | return nil 852 | } 853 | 854 | // DeleteSong deletes the specified Song in the 855 | // specified Album and Artist only in the database 856 | // and returns nil if there was no error. 857 | func DeleteSong(artist, album, song, mPoint string) error { 858 | glog.Infof("Deleting song: %s with Artist: %s and Album: %s\n", song, artist, album) 859 | if song[0] == '.' { 860 | return nil 861 | } 862 | 863 | db, err := bolt.Open(config.DbPath, 0600, nil) 864 | if err != nil { 865 | return fuse.EIO 866 | } 867 | defer db.Close() 868 | 869 | var songData SongStore 870 | err = db.Update(func(tx *bolt.Tx) error { 871 | root := tx.Bucket([]byte("Artists")) 872 | artistBucket := root.Bucket([]byte(artist)) 873 | if artistBucket == nil { 874 | return errors.New("Artist not found.") 875 | } 876 | 877 | albumBucket := artistBucket.Bucket([]byte(album)) 878 | if albumBucket == nil { 879 | return errors.New("Album not found.") 880 | } 881 | 882 | songJson := albumBucket.Get([]byte(song)) 883 | if songJson != nil { 884 | err := json.Unmarshal(songJson, &songData) 885 | if err != nil { 886 | os.Remove(songData.SongFullPath) 887 | } 888 | } 889 | return albumBucket.Delete([]byte(song)) 890 | }) 891 | 892 | if err != nil { 893 | return err 894 | } 895 | 896 | if songData.Playlists != nil { 897 | for _, list := range songData.Playlists { 898 | DeletePlaylistSong(list, song, true) 899 | RegeneratePlaylistFile(list, mPoint) 900 | } 901 | } 902 | return nil 903 | } 904 | -------------------------------------------------------------------------------- /testing/testMuLi.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Danko Miocevic. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Author: Danko Miocevic 16 | 17 | # This script tests the MuLi filesystem. 18 | # For more information please read the README.md contained on 19 | # this folder. 20 | 21 | # Config options 22 | MULI_X='../mulifs -alsologtostderr' 23 | SRC_DIR='./testSrc' 24 | DST_DIR='./testDst' 25 | PWD_DIR=$(pwd) 26 | TEST_SIZE=2 27 | 28 | # Set Colors 29 | RED=`tput setaf 1` 30 | GREEN=`tput setaf 2` 31 | NC=`tput sgr0` 32 | 33 | # Tag setting function 34 | # This function is used to set the tags to the MP3 files. 35 | # The first argument is the file. 36 | # The second argument is the Artist. 37 | # The third argument is the Album. 38 | # The fourth argument is the Title. 39 | # This function can be modified in order to use another 40 | # tag editor command. 41 | # Returns the id3 command return value. 42 | function set_tags { 43 | id3tag --artist="$2" --album="$3" --song="$4" $1 &> /dev/null 44 | return $? 45 | } 46 | 47 | # Tag stripping function 48 | # This function should strip all the tags from the MP3 file. 49 | # The fist argument is the file. 50 | function strip_tags { 51 | id3convert -s $1 &> /dev/null 52 | return $? 53 | } 54 | 55 | # Tag checking function 56 | # This function is used to check that the tags are correct 57 | # in a specific MP3 file. 58 | # The first argument is the file. 59 | # The second argument is the Artist. 60 | # The third argument is the Album. 61 | # The fourth argument is the Title. 62 | # This function can be modified in order to use another 63 | # tag editor command. 64 | # Returns 0 if the tags are correct. 65 | function check_tags { 66 | local ARTIST=$(id3info $1 | grep TPE1) 67 | local ALBUM=$(id3info $1 | grep TALB) 68 | local TITLE=$(id3info $1 | grep TIT2) 69 | if [ ${ARTIST#*:} != $2 ]; then 70 | return 1 71 | fi 72 | 73 | if [ ${ALBUM#*:} != $3 ]; then 74 | return 2 75 | fi 76 | 77 | if [ ${TITLE#*:} != $4 ]; then 78 | return 3 79 | fi 80 | return 0 81 | } 82 | 83 | # Create empty MP3 function 84 | function create_empty { 85 | cd $PWD_DIR 86 | echo -n "Creating empty MP3 files..." 87 | local HAS_ERROR=0 88 | 89 | cp test.mp3 $SRC_DIR/empty.mp3 &> /dev/null 90 | strip_tags $SRC_DIR/empty.mp3 91 | #TODO: Maybe test here what happens if we only add some tags 92 | # like Artist or Album only. 93 | if [ $HAS_ERROR -eq 0 ] ; then 94 | echo "${GREEN}OK!${NC}" 95 | fi 96 | } 97 | 98 | # Check that empty MP3 exists function 99 | function check_empty { 100 | cd $PWD_DIR 101 | cd $DST_DIR 102 | echo -n "Checking empty MP3 files..." 103 | local HAS_ERROR=0 104 | 105 | if [ ! -d "unknown" ] ; then 106 | echo "${RED}ERROR${NC}" 107 | echo "The unknown Artist directory does not exist." 108 | return 1 109 | fi 110 | cd unknown 111 | 112 | if [ ! -d "unknown" ] ; then 113 | echo "${RED}ERROR${NC}" 114 | echo "The unknown Album directory does not exist." 115 | return 2 116 | fi 117 | cd unknown 118 | 119 | if [ -f "empty.mp3" ] ; then 120 | check_tags empty.mp3 unknown unknown empty 121 | if [ $? -ne 0 ] ; then 122 | if [ $HAS_ERROR -eq 0 ] ; then 123 | echo "${RED}ERROR${NC}" 124 | fi 125 | HAS_ERROR=1 126 | echo "ERROR: File unknown/unknown/empty.mp3 tags not match" 127 | fi 128 | else 129 | if [ $HAS_ERROR -eq 0 ] ; then 130 | echo "${RED}ERROR${NC}" 131 | fi 132 | HAS_ERROR=1 133 | echo "The empty.mp3 file cannot be found." 134 | fi 135 | 136 | if [ $HAS_ERROR -eq 0 ] ; then 137 | echo "${GREEN}OK!${NC}" 138 | fi 139 | } 140 | 141 | # Create MP3 with special characters function 142 | function create_special { 143 | cd $PWD_DIR 144 | echo -n "Creating special MP3 files..." 145 | local HAS_ERROR=0 146 | 147 | cp test.mp3 $SRC_DIR/special.mp3 &> /dev/null 148 | set_tags $SRC_DIR/special.mp3 'Increíble Artista' 'Suco de Aça' 'Canció' 149 | #TODO: Maybe test here what happens if we only add some tags 150 | # like Artist or Album only. 151 | if [ $HAS_ERROR -eq 0 ] ; then 152 | echo "${GREEN}OK!${NC}" 153 | fi 154 | } 155 | 156 | # Check that MP3 with special characters 157 | # exists function 158 | function check_special { 159 | cd $PWD_DIR 160 | cd $DST_DIR 161 | echo -n "Checking special MP3 files..." 162 | local HAS_ERROR=0 163 | 164 | if [ ! -d "Increible_Artista" ] ; then 165 | echo "${RED}ERROR${NC}" 166 | echo "The special Artist directory does not exist." 167 | return 1 168 | fi 169 | cd Increible_Artista 170 | 171 | if [ ! -d "Suco_de_Acai" ] ; then 172 | echo "${RED}ERROR${NC}" 173 | echo "The special Album directory does not exist." 174 | return 2 175 | fi 176 | cd Suco_de_Acai 177 | 178 | if [ -f "Cancion.mp3" ] ; then 179 | check_tags $SRC_DIR/special.mp3 "Increíble Artista!" "Suco de Açaí" "Canción" 180 | if [ $? -ne 0 ] ; then 181 | if [ $HAS_ERROR -eq 0 ] ; then 182 | echo "${RED}ERROR${NC}" 183 | fi 184 | HAS_ERROR=1 185 | echo "ERROR: File Increible_Artista/Suco_de_Acai/Cancion.mp3 tags not match" 186 | fi 187 | else 188 | if [ $HAS_ERROR -eq 0 ] ; then 189 | echo "${RED}ERROR${NC}" 190 | fi 191 | HAS_ERROR=1 192 | echo "The Cancion.mp3 file cannot be found." 193 | fi 194 | 195 | if [ $HAS_ERROR -eq 0 ] ; then 196 | echo "${GREEN}OK!${NC}" 197 | fi 198 | } 199 | 200 | # Create fake MP3s function 201 | function create_fake { 202 | cd $PWD_DIR 203 | echo -n "Creating lots of fake files..." 204 | local ARTIST_COUNT=$TEST_SIZE 205 | local HAS_ERROR=0 206 | 207 | while [ $ARTIST_COUNT -gt 0 ]; do 208 | local ALBUM_COUNT=$TEST_SIZE 209 | while [ $ALBUM_COUNT -gt 0 ]; do 210 | local SONG_COUNT=$TEST_SIZE 211 | while [ $SONG_COUNT -gt 0 ]; do 212 | cp "test.mp3" "$SRC_DIR/testAr${ARTIST_COUNT}Al${ALBUM_COUNT}Sn${SONG_COUNT}.mp3" &> /dev/null 213 | set_tags "$SRC_DIR/testAr${ARTIST_COUNT}Al${ALBUM_COUNT}Sn${SONG_COUNT}.mp3" "GreatArtist$ARTIST_COUNT" "GreatAlbum$ALBUM_COUNT" "Song$SONG_COUNT" 214 | if [ $? -ne 0 ]; then 215 | if [ $HAS_ERROR -eq 0 ] ; then 216 | echo "${RED}ERROR${NC}" 217 | fi 218 | HAS_ERROR=1 219 | echo "ERROR in file $SRC_DIR/testAr${ARTIST_COUNT}Al${ALBUM_COUNT}Sn${SONG_COUNT}.mp3" 220 | fi 221 | let SONG_COUNT=SONG_COUNT-1 222 | done 223 | let ALBUM_COUNT=ALBUM_COUNT-1 224 | done 225 | let ARTIST_COUNT=ARTIST_COUNT-1 226 | done 227 | 228 | if [ $HAS_ERROR -eq 0 ] ; then 229 | echo "${GREEN}OK!${NC}" 230 | fi 231 | } 232 | 233 | # Create fake MP3s function 234 | function drop_files { 235 | cd $PWD_DIR 236 | echo -n "Droping files..." 237 | local ARTIST_COUNT=$TEST_SIZE 238 | local HAS_ERROR=0 239 | 240 | while [ $ARTIST_COUNT -gt 0 ]; do 241 | local ALBUM_COUNT=$TEST_SIZE 242 | while [ $ALBUM_COUNT -gt 0 ]; do 243 | local SONG_COUNT=$TEST_SIZE 244 | while [ $SONG_COUNT -gt 0 ]; do 245 | cp "test.mp3" "$DST_DIR/drop/Artist${ARTIST_COUNT}Album${ALBUM_COUNT}Song${SONG_COUNT}.mp3" &> /dev/null 246 | set_tags "$DST_DIR/drop/Artist${ARTIST_COUNT}Album${ALBUM_COUNT}Song${SONG_COUNT}.mp3" "GreatArtist$ARTIST_COUNT" "GreatAlbum$ALBUM_COUNT" "Song$SONG_COUNT" 247 | if [ $? -ne 0 ]; then 248 | if [ $HAS_ERROR -eq 0 ] ; then 249 | echo "${RED}ERROR${NC}" 250 | fi 251 | HAS_ERROR=1 252 | echo "ERROR in file $DST_DIR/drop/Artist${ARTIST_COUNT}Album${ALBUM_COUNT}Song${SONG_COUNT}.mp3" 253 | fi 254 | let SONG_COUNT=SONG_COUNT-1 255 | done 256 | let ALBUM_COUNT=ALBUM_COUNT-1 257 | done 258 | let ARTIST_COUNT=ARTIST_COUNT-1 259 | done 260 | 261 | if [ $HAS_ERROR -eq 0 ] ; then 262 | echo "${GREEN}OK!${NC}" 263 | fi 264 | sleep 5 265 | } 266 | 267 | # Check that all the files were in the right place. 268 | function check_fake { 269 | cd $PWD_DIR 270 | echo -n "Checking the fake files..." 271 | local ARTIST_COUNT=$TEST_SIZE 272 | local HAS_ERROR=0 273 | 274 | while [ $ARTIST_COUNT -gt 0 ]; do 275 | cd $PWD_DIR 276 | if [ -d "$DST_DIR/GreatArtist$ARTIST_COUNT" ]; then 277 | cd "$DST_DIR/GreatArtist$ARTIST_COUNT" 278 | local ALBUM_COUNT=$TEST_SIZE 279 | while [ $ALBUM_COUNT -gt 0 ]; do 280 | if [ -d "GreatAlbum$ALBUM_COUNT" ]; then 281 | cd "GreatAlbum$ALBUM_COUNT" 282 | local SONG_COUNT=$TEST_SIZE 283 | while [ $SONG_COUNT -gt 0 ]; do 284 | if [ -f "Song${SONG_COUNT}.mp3" ]; then 285 | if [ ! -s "Song${SONG_COUNT}.mp3" ]; then 286 | if [ $HAS_ERROR -eq 0 ] ; then 287 | echo "${RED}ERROR${NC}" 288 | fi 289 | HAS_ERROR=1 290 | echo "ERROR: File ${DST_DIR}/GreatArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 has 0 size" 291 | fi 292 | else 293 | if [ $HAS_ERROR -eq 0 ] ; then 294 | echo "${RED}ERROR${NC}" 295 | fi 296 | HAS_ERROR=1 297 | echo "ERROR: File ${DST_DIR}/GreatArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 not exists" 298 | fi 299 | let SONG_COUNT=SONG_COUNT-1 300 | done 301 | cd .. 302 | else 303 | if [ $HAS_ERROR -eq 0 ] ; then 304 | echo "${RED}ERROR${NC}" 305 | fi 306 | HAS_ERROR=1 307 | echo "ERROR: Directory ${DST_DIR}/GreatArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT} not exists" 308 | fi 309 | let ALBUM_COUNT=ALBUM_COUNT-1 310 | done 311 | else 312 | if [ $HAS_ERROR -eq 0 ] ; then 313 | echo "${RED}ERROR${NC}" 314 | fi 315 | HAS_ERROR=1 316 | echo "ERROR: Directory ${DST_DIR}/GreatArtist${ARTIST_COUNT} not exists" 317 | fi 318 | let ARTIST_COUNT=ARTIST_COUNT-1 319 | done 320 | 321 | if [ $HAS_ERROR -eq 0 ] ; then 322 | echo "${GREEN}OK!${NC}" 323 | fi 324 | } 325 | 326 | # function to create directories and fill them with empty files 327 | function mkdirs { 328 | cd $PWD_DIR 329 | echo -n "Creating dirs..." 330 | local ARTIST_COUNT=$TEST_SIZE 331 | local HAS_ERROR=0 332 | 333 | while [ $ARTIST_COUNT -gt 0 ]; do 334 | cd $PWD_DIR 335 | mkdir "$DST_DIR/NewArtist$ARTIST_COUNT" &> /dev/null 336 | if [ $? -eq 0 ]; then 337 | cd "$DST_DIR/NewArtist$ARTIST_COUNT" 338 | local ALBUM_COUNT=$TEST_SIZE 339 | while [ $ALBUM_COUNT -gt 0 ]; do 340 | mkdir "NewAlbum$ALBUM_COUNT" &> /dev/null 341 | if [ $? -eq 0 ]; then 342 | cd "NewAlbum$ALBUM_COUNT" 343 | local SONG_COUNT=$TEST_SIZE 344 | while [ $SONG_COUNT -gt 0 ]; do 345 | cp $PWD_DIR/test.mp3 NewSong${SONG_COUNT}.mp3 &> /dev/null 346 | strip_tags NewSong${SONG_COUNT}.mp3 347 | let SONG_COUNT=SONG_COUNT-1 348 | done 349 | cd .. 350 | else 351 | if [ $HAS_ERROR -eq 0 ] ; then 352 | echo "${RED}ERROR${NC}" 353 | fi 354 | HAS_ERROR=1 355 | echo "ERROR: Directory ${DST_DIR}/NewArtist${ARTIST_COUNT}/NewAlbum${ALBUM_COUNT} cannot be created" 356 | fi 357 | let ALBUM_COUNT=ALBUM_COUNT-1 358 | done 359 | else 360 | if [ $HAS_ERROR -eq 0 ] ; then 361 | echo "${RED}ERROR${NC}" 362 | fi 363 | HAS_ERROR=1 364 | echo "ERROR: Directory ${DST_DIR}/NewArtist${ARTIST_COUNT} cannot be created" 365 | fi 366 | let ARTIST_COUNT=ARTIST_COUNT-1 367 | done 368 | 369 | if [ $HAS_ERROR -eq 0 ] ; then 370 | echo "${GREEN}OK!${NC}" 371 | fi 372 | } 373 | 374 | 375 | 376 | # Check that all the created files are in the right place. 377 | function check_mkdirs { 378 | cd $PWD_DIR 379 | echo -n "Checking the created files..." 380 | local ARTIST_COUNT=$TEST_SIZE 381 | local HAS_ERROR=0 382 | 383 | while [ $ARTIST_COUNT -gt 0 ]; do 384 | cd $PWD_DIR 385 | if [ -d "$DST_DIR/NewArtist$ARTIST_COUNT" ]; then 386 | cd "$DST_DIR/NewArtist$ARTIST_COUNT" 387 | local ALBUM_COUNT=$TEST_SIZE 388 | while [ $ALBUM_COUNT -gt 0 ]; do 389 | if [ -d "NewAlbum$ALBUM_COUNT" ]; then 390 | cd "NewAlbum$ALBUM_COUNT" 391 | local SONG_COUNT=$TEST_SIZE 392 | while [ $SONG_COUNT -gt 0 ]; do 393 | if [ -f "NewSong${SONG_COUNT}.mp3" ]; then 394 | if [ ! -s "NewSong${SONG_COUNT}.mp3" ]; then 395 | if [ $HAS_ERROR -eq 0 ] ; then 396 | echo "${RED}ERROR${NC}" 397 | fi 398 | HAS_ERROR=1 399 | echo "ERROR: File ${DST_DIR}/NewArtist${ARTIST_COUNT}/NewAlbum${ALBUM_COUNT}/NewSong${SONG_COUNT}.mp3 has 0 size" 400 | else 401 | check_tags NewSong${SONG_COUNT}.mp3 NewArtist${ARTIST_COUNT} NewAlbum${ALBUM_COUNT} NewSong${SONG_COUNT} 402 | if [ $? -ne 0 ] ; then 403 | if [ $HAS_ERROR -eq 0 ] ; then 404 | echo "${RED}ERROR${NC}" 405 | fi 406 | HAS_ERROR=1 407 | echo "ERROR: File ${DST_DIR}/NewArtist${ARTIST_COUNT}/NewAlbum${ALBUM_COUNT}/NewSong${SONG_COUNT}.mp3 tags are wrong." 408 | fi 409 | fi 410 | else 411 | if [ $HAS_ERROR -eq 0 ] ; then 412 | echo "${RED}ERROR${NC}" 413 | fi 414 | HAS_ERROR=1 415 | echo "ERROR: File ${DST_DIR}/NewArtist${ARTIST_COUNT}/NewAlbum${ALBUM_COUNT}/NewSong${SONG_COUNT}.mp3 not exists" 416 | fi 417 | let SONG_COUNT=SONG_COUNT-1 418 | done 419 | cd .. 420 | else 421 | if [ $HAS_ERROR -eq 0 ] ; then 422 | echo "${RED}ERROR${NC}" 423 | fi 424 | HAS_ERROR=1 425 | echo "ERROR: Directory ${DST_DIR}/NewArtist${ARTIST_COUNT}/NewAlbum${ALBUM_COUNT} not exists" 426 | fi 427 | let ALBUM_COUNT=ALBUM_COUNT-1 428 | done 429 | else 430 | if [ $HAS_ERROR -eq 0 ] ; then 431 | echo "${RED}ERROR${NC}" 432 | fi 433 | HAS_ERROR=1 434 | echo "ERROR: Directory ${DST_DIR}/NewArtist${ARTIST_COUNT} not exists" 435 | fi 436 | let ARTIST_COUNT=ARTIST_COUNT-1 437 | done 438 | 439 | if [ $HAS_ERROR -eq 0 ] ; then 440 | echo "${GREEN}OK!${NC}" 441 | fi 442 | } 443 | 444 | # function to create directories inside playlist folder 445 | # and fill them with songs. 446 | function playlist_mkdirs { 447 | cd $PWD_DIR 448 | echo -n "Creating dirs inside playlist..." 449 | local ARTIST_COUNT=$TEST_SIZE 450 | local HAS_ERROR=0 451 | 452 | while [ $ARTIST_COUNT -gt 0 ]; do 453 | cd $PWD_DIR 454 | mkdir "$DST_DIR/playlists/NewList$ARTIST_COUNT" 455 | if [ $? -eq 0 ] ; then 456 | cd "$DST_DIR/playlists/NewList$ARTIST_COUNT" 457 | local ALBUM_COUNT=$TEST_SIZE 458 | while [ $ALBUM_COUNT -gt 0 ]; do 459 | local SONG_COUNT=$TEST_SIZE 460 | while [ $SONG_COUNT -gt 0 ]; do 461 | cp $PWD_DIR/test.mp3 NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3 &> /dev/null 462 | set_tags NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3 ListArtist${ARTIST_COUNT} ListAlbum${ALBUM_COUNT} NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT} 463 | sleep 1 464 | let SONG_COUNT=SONG_COUNT-1 465 | done 466 | let ALBUM_COUNT=ALBUM_COUNT-1 467 | done 468 | else 469 | if [ $HAS_ERROR -eq 0 ] ; then 470 | echo "${RED}ERROR${NC}" 471 | fi 472 | HAS_ERROR=1 473 | echo "ERROR: Directory ${DST_DIR}/playlists/NewList${ARTIST_COUNT} cannot be created" 474 | fi 475 | let ARTIST_COUNT=ARTIST_COUNT-1 476 | done 477 | 478 | if [ $HAS_ERROR -eq 0 ] ; then 479 | echo "${GREEN}OK!${NC}" 480 | fi 481 | } 482 | 483 | # Check that all the created files inside playlists directory 484 | # are in the right place. 485 | function check_playlists_mkdirs { 486 | cd $PWD_DIR 487 | echo -n "Checking the created files inside playlists..." 488 | local ARTIST_COUNT=$TEST_SIZE 489 | local HAS_ERROR=0 490 | 491 | while [ $ARTIST_COUNT -gt 0 ]; do 492 | cd $PWD_DIR 493 | if [ -d "$DST_DIR/ListArtist$ARTIST_COUNT" ]; then 494 | cd "$DST_DIR/ListArtist$ARTIST_COUNT" 495 | local ALBUM_COUNT=$TEST_SIZE 496 | while [ $ALBUM_COUNT -gt 0 ]; do 497 | if [ -d "ListAlbum$ALBUM_COUNT" ]; then 498 | cd "ListAlbum$ALBUM_COUNT" 499 | local SONG_COUNT=$TEST_SIZE 500 | while [ $SONG_COUNT -gt 0 ]; do 501 | if [ -f "NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3" ]; then 502 | if [ ! -s "NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3" ]; then 503 | if [ $HAS_ERROR -eq 0 ] ; then 504 | echo "${RED}ERROR${NC}" 505 | fi 506 | HAS_ERROR=1 507 | echo "ERROR: File ${DST_DIR}/ListArtist${ARTIST_COUNT}/ListAlbum${ALBUM_COUNT}/NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3 has 0 size" 508 | else 509 | check_tags NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3 ListArtist${ARTIST_COUNT} ListAlbum${ALBUM_COUNT} NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT} 510 | if [ $? -ne 0 ] ; then 511 | if [ $HAS_ERROR -eq 0 ] ; then 512 | echo "${RED}ERROR${NC}" 513 | fi 514 | HAS_ERROR=1 515 | echo "ERROR: File ${DST_DIR}/ListArtist${ARTIST_COUNT}/ListAlbum${ALBUM_COUNT}/NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3 tags are wrong." 516 | fi 517 | fi 518 | else 519 | if [ $HAS_ERROR -eq 0 ] ; then 520 | echo "${RED}ERROR${NC}" 521 | fi 522 | HAS_ERROR=1 523 | echo "ERROR: File ${DST_DIR}/ListArtist${ARTIST_COUNT}/ListAlbum${ALBUM_COUNT}/NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3 not exists" 524 | fi 525 | let SONG_COUNT=SONG_COUNT-1 526 | done 527 | cd .. 528 | else 529 | if [ $HAS_ERROR -eq 0 ] ; then 530 | echo "${RED}ERROR${NC}" 531 | fi 532 | HAS_ERROR=1 533 | echo "ERROR: Directory ${DST_DIR}/ListArtist${ARTIST_COUNT}/ListAlbum${ALBUM_COUNT} not exists" 534 | fi 535 | let ALBUM_COUNT=ALBUM_COUNT-1 536 | done 537 | else 538 | if [ $HAS_ERROR -eq 0 ] ; then 539 | echo "${RED}ERROR${NC}" 540 | fi 541 | HAS_ERROR=1 542 | echo "ERROR: Directory ${DST_DIR}/ListArtist${ARTIST_COUNT} not exists" 543 | fi 544 | let ARTIST_COUNT=ARTIST_COUNT-1 545 | done 546 | 547 | # Now test the playlist files. 548 | ARTIST_COUNT=$TEST_SIZE 549 | 550 | while [ $ARTIST_COUNT -gt 0 ]; do 551 | cd $PWD_DIR 552 | echo "#EXTM3U" > ${SRC_DIR}/NewList${ARTIST_COUNT} 553 | cd $SRC_DIR 554 | local BASE_DIR=$(pwd) 555 | cd $PWD_DIR 556 | ALBUM_COUNT=1 557 | while [ $ALBUM_COUNT -le $TEST_SIZE ]; do 558 | SONG_COUNT=1 559 | while [ $SONG_COUNT -le $TEST_SIZE ]; do 560 | echo "#MULI ListArtist${ARTIST_COUNT} - ListAlbum${ALBUM_COUNT} - NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3" >> ${SRC_DIR}/NewList${ARTIST_COUNT} 561 | echo "${BASE_DIR}/ListArtist${ARTIST_COUNT}/ListAlbum${ALBUM_COUNT}/NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3" >> ${SRC_DIR}/NewList${ARTIST_COUNT} 562 | echo >> ${SRC_DIR}/NewList${ARTIST_COUNT} 563 | let SONG_COUNT=SONG_COUNT+1 564 | done 565 | let ALBUM_COUNT=ALBUM_COUNT+1 566 | done 567 | cmp --silent ${SRC_DIR}/NewList${ARTIST_COUNT} ${SRC_DIR}/playlists/NewList${ARTIST_COUNT}.m3u 568 | if [ ! $? -eq 0 ] ; then 569 | if [ $HAS_ERROR -eq 0 ] ; then 570 | echo "${RED}ERROR${NC}" 571 | fi 572 | HAS_ERROR=1 573 | echo "ERROR: The playlist ${SRC_DIR}/playlists/NewList${ARTIST_COUNT}.m3u has errors." 574 | fi 575 | let ARTIST_COUNT=ARTIST_COUNT-1 576 | done 577 | 578 | if [ $HAS_ERROR -eq 0 ] ; then 579 | echo "${GREEN}OK!${NC}" 580 | fi 581 | } 582 | 583 | # Moves all the created playlists directories 584 | function move_playlists_dirs { 585 | cd $PWD_DIR 586 | echo -n "Moving the created files inside playlists..." 587 | local ARTIST_COUNT=$TEST_SIZE 588 | local HAS_ERROR=0 589 | 590 | while [ $ARTIST_COUNT -gt 0 ]; do 591 | cd $PWD_DIR 592 | if [ -d "$DST_DIR/playlists/NewList$ARTIST_COUNT" ]; then 593 | mv "$DST_DIR/playlists/NewList$ARTIST_COUNT" "$DST_DIR/playlists/SomeList$ARTIST_COUNT" &> /dev/null 594 | if [ $? -ne 0 ] ; then 595 | if [ $HAS_ERROR -eq 0 ] ; then 596 | echo "${RED}ERROR${NC}" 597 | fi 598 | HAS_ERROR=1 599 | echo "ERROR: Cannot move to ${DST_DIR}/playlists/SomeList${ARTIST_COUNT}" 600 | fi 601 | else 602 | if [ $HAS_ERROR -eq 0 ] ; then 603 | echo "${RED}ERROR${NC}" 604 | fi 605 | HAS_ERROR=1 606 | echo "ERROR: Directory ${DST_DIR}/NewList${ARTIST_COUNT} not exists" 607 | fi 608 | let ARTIST_COUNT=ARTIST_COUNT-1 609 | done 610 | 611 | if [ $HAS_ERROR -eq 0 ] ; then 612 | echo "${GREEN}OK!${NC}" 613 | fi 614 | } 615 | 616 | # Moves all the created files inside playlists directory 617 | function move_playlists_files { 618 | cd $PWD_DIR 619 | echo -n "Moving the files inside playlists..." 620 | local ARTIST_COUNT=$TEST_SIZE 621 | local HAS_ERROR=0 622 | 623 | while [ $ARTIST_COUNT -gt 0 ]; do 624 | cd $PWD_DIR 625 | if [ -d "$DST_DIR/playlists/SomeList$ARTIST_COUNT" ]; then 626 | mkdir "$DST_DIR/playlists/OtherList$ARTIST_COUNT" 627 | if [ $? -ne 0 ] ; then 628 | if [ $HAS_ERROR -eq 0 ] ; then 629 | echo "${RED}ERROR${NC}" 630 | fi 631 | HAS_ERROR=1 632 | echo "ERROR: Cannot create ${DST_DIR}/playlists/OtherList${ARTIST_COUNT}" 633 | else 634 | mv "$DST_DIR/playlists/SomeList$ARTIST_COUNT/*" "$DST_DIR/playlists/OtherList$ARTIST_COUNT" 635 | if [ $? -ne 0 ] ; then 636 | if [ $HAS_ERROR -eq 0 ] ; then 637 | echo "${RED}ERROR${NC}" 638 | fi 639 | HAS_ERROR=1 640 | echo "ERROR: Cannot move files to ${DST_DIR}/playlists/OtherList${ARTIST_COUNT}" 641 | fi 642 | fi 643 | else 644 | if [ $HAS_ERROR -eq 0 ] ; then 645 | echo "${RED}ERROR${NC}" 646 | fi 647 | HAS_ERROR=1 648 | echo "ERROR: Directory ${DST_DIR}/playlists/SomeList${ARTIST_COUNT} not exists" 649 | fi 650 | let ARTIST_COUNT=ARTIST_COUNT-1 651 | done 652 | 653 | if [ $HAS_ERROR -eq 0 ] ; then 654 | echo "${GREEN}OK!${NC}" 655 | fi 656 | } 657 | 658 | # Check that all the moved playlist dirs inside playlists directory 659 | # are in the right place. 660 | function check_moved_playlists_files { 661 | cd $PWD_DIR 662 | echo -n "Checking the moved files inside playlists..." 663 | 664 | # Test the playlist files. 665 | local ARTIST_COUNT=$TEST_SIZE 666 | local HAS_ERROR=0 667 | 668 | while [ $ARTIST_COUNT -gt 0 ]; do 669 | cd $PWD_DIR 670 | echo "#EXTM3U" > ${SRC_DIR}/OtherList${ARTIST_COUNT} 671 | cd $SRC_DIR 672 | local BASE_DIR=$(pwd) 673 | cd $PWD_DIR 674 | ALBUM_COUNT=1 675 | while [ $ALBUM_COUNT -le $TEST_SIZE ]; do 676 | SONG_COUNT=1 677 | while [ $SONG_COUNT -le $TEST_SIZE ]; do 678 | echo "#MULI ListArtist${ARTIST_COUNT} - ListAlbum${ALBUM_COUNT} - NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3" >> ${SRC_DIR}/OtherList${ARTIST_COUNT} 679 | echo "${BASE_DIR}/ListArtist${ARTIST_COUNT}/ListAlbum${ALBUM_COUNT}/NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3" >> ${SRC_DIR}/OtherList${ARTIST_COUNT} 680 | echo >> ${SRC_DIR}/OtherList${ARTIST_COUNT} 681 | let SONG_COUNT=SONG_COUNT+1 682 | done 683 | let ALBUM_COUNT=ALBUM_COUNT+1 684 | done 685 | cmp --silent ${SRC_DIR}/OtherList${ARTIST_COUNT} ${SRC_DIR}/playlists/OtherList${ARTIST_COUNT}.m3u 686 | if [ ! $? -eq 0 ] ; then 687 | if [ $HAS_ERROR -eq 0 ] ; then 688 | echo "${RED}ERROR${NC}" 689 | fi 690 | HAS_ERROR=1 691 | echo "ERROR: The playlist ${SRC_DIR}/playlists/OtherList${ARTIST_COUNT}.m3u has errors." 692 | fi 693 | let ARTIST_COUNT=ARTIST_COUNT-1 694 | done 695 | 696 | if [ $HAS_ERROR -eq 0 ] ; then 697 | echo "${GREEN}OK!${NC}" 698 | fi 699 | } 700 | 701 | # Check that all the moved playlist dirs inside playlists directory 702 | # are in the right place. 703 | function check_moved_playlists_dirs { 704 | cd $PWD_DIR 705 | echo -n "Checking the created files inside playlists..." 706 | 707 | # Test the playlist files. 708 | local ARTIST_COUNT=$TEST_SIZE 709 | local HAS_ERROR=0 710 | 711 | while [ $ARTIST_COUNT -gt 0 ]; do 712 | cd $PWD_DIR 713 | echo "#EXTM3U" > ${SRC_DIR}/SomeList${ARTIST_COUNT} 714 | cd $SRC_DIR 715 | local BASE_DIR=$(pwd) 716 | cd $PWD_DIR 717 | ALBUM_COUNT=1 718 | while [ $ALBUM_COUNT -le $TEST_SIZE ]; do 719 | SONG_COUNT=1 720 | while [ $SONG_COUNT -le $TEST_SIZE ]; do 721 | echo "#MULI ListArtist${ARTIST_COUNT} - ListAlbum${ALBUM_COUNT} - NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3" >> ${SRC_DIR}/SomeList${ARTIST_COUNT} 722 | echo "${BASE_DIR}/ListArtist${ARTIST_COUNT}/ListAlbum${ALBUM_COUNT}/NewSongA${ARTIST_COUNT}A${ALBUM_COUNT}S${SONG_COUNT}.mp3" >> ${SRC_DIR}/SomeList${ARTIST_COUNT} 723 | echo >> ${SRC_DIR}/SomeList${ARTIST_COUNT} 724 | let SONG_COUNT=SONG_COUNT+1 725 | done 726 | let ALBUM_COUNT=ALBUM_COUNT+1 727 | done 728 | cmp --silent ${SRC_DIR}/SomeList${ARTIST_COUNT} ${SRC_DIR}/playlists/SomeList${ARTIST_COUNT}.m3u 729 | if [ ! $? -eq 0 ] ; then 730 | if [ $HAS_ERROR -eq 0 ] ; then 731 | echo "${RED}ERROR${NC}" 732 | fi 733 | HAS_ERROR=1 734 | echo "ERROR: The playlist ${SRC_DIR}/playlists/SomeList${ARTIST_COUNT}.m3u has errors." 735 | fi 736 | let ARTIST_COUNT=ARTIST_COUNT-1 737 | done 738 | 739 | if [ $HAS_ERROR -eq 0 ] ; then 740 | echo "${GREEN}OK!${NC}" 741 | fi 742 | } 743 | 744 | # Move artists 745 | function move_artists { 746 | cd $PWD_DIR 747 | cd $DST_DIR 748 | echo -n "Moving Artists..." 749 | local ARTIST_COUNT=$TEST_SIZE 750 | 751 | while [ $ARTIST_COUNT -gt 0 ] ; do 752 | if [ -d "GreatArtist$ARTIST_COUNT" ]; then 753 | mv "GreatArtist$ARTIST_COUNT" "DifferentArtist$ARTIST_COUNT" &> /dev/null 754 | fi 755 | let ARTIST_COUNT=ARTIST_COUNT-1 756 | done 757 | 758 | echo "${GREEN}OK!${NC}" 759 | } 760 | 761 | # Check moved Artists 762 | function check_moved_artists { 763 | cd $PWD_DIR 764 | cd $DST_DIR 765 | echo -n "Checking moved Artists..." 766 | 767 | local ARTIST_COUNT=$TEST_SIZE 768 | local HAS_ERROR=0 769 | while [ $ARTIST_COUNT -gt 0 ]; do 770 | cd $PWD_DIR 771 | if [ -d "$DST_DIR/DifferentArtist$ARTIST_COUNT" ]; then 772 | cd "$DST_DIR/DifferentArtist$ARTIST_COUNT" 773 | local ALBUM_COUNT=$TEST_SIZE 774 | while [ $ALBUM_COUNT -gt 0 ]; do 775 | if [ -d "GreatAlbum$ALBUM_COUNT" ]; then 776 | cd "GreatAlbum$ALBUM_COUNT" 777 | local SONG_COUNT=$TEST_SIZE 778 | while [ $SONG_COUNT -gt 0 ]; do 779 | if [ -f "Song${SONG_COUNT}.mp3" ]; then 780 | if [ ! -s "Song${SONG_COUNT}.mp3" ]; then 781 | if [ $HAS_ERROR -eq 0 ] ; then 782 | echo "${RED}ERROR${NC}" 783 | fi 784 | HAS_ERROR=1 785 | echo "ERROR: File ${DST_DIR}/DifferentArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 has 0 size" 786 | else 787 | check_tags Song${SONG_COUNT}.mp3 DifferentArtist$ARTIST_COUNT GreatAlbum$ALBUM_COUNT Song${SONG_COUNT} 788 | if [ $? -ne 0 ] ; then 789 | if [ $HAS_ERROR -eq 0 ] ; then 790 | echo "${RED}ERROR${NC}" 791 | fi 792 | HAS_ERROR=1 793 | echo "ERROR: File ${DST_DIR}/DifferentArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 tags not match" 794 | fi 795 | fi 796 | else 797 | if [ $HAS_ERROR -eq 0 ] ; then 798 | echo "${RED}ERROR${NC}" 799 | fi 800 | HAS_ERROR=1 801 | echo "ERROR: File ${DST_DIR}/DifferentArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 not exists" 802 | fi 803 | let SONG_COUNT=SONG_COUNT-1 804 | done 805 | cd .. 806 | else 807 | if [ $HAS_ERROR -eq 0 ] ; then 808 | echo "${RED}ERROR${NC}" 809 | fi 810 | HAS_ERROR=1 811 | echo "ERROR: Directory ${DST_DIR}/DifferentArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT} not exists" 812 | fi 813 | let ALBUM_COUNT=ALBUM_COUNT-1 814 | done 815 | else 816 | if [ $HAS_ERROR -eq 0 ] ; then 817 | echo "${RED}ERROR${NC}" 818 | fi 819 | HAS_ERROR=1 820 | echo "ERROR: Directory ${DST_DIR}/DifferentArtist${ARTIST_COUNT} not exists" 821 | fi 822 | let ARTIST_COUNT=ARTIST_COUNT-1 823 | done 824 | 825 | if [ $HAS_ERROR -eq 0 ] ; then 826 | echo "${GREEN}OK!${NC}" 827 | fi 828 | } 829 | 830 | # Copy artists around 831 | function copy_artists { 832 | cd $PWD_DIR 833 | cd $DST_DIR 834 | echo -n "Copying Artists around..." 835 | local ARTIST_COUNT=$TEST_SIZE 836 | 837 | while [ $ARTIST_COUNT -gt 0 ] ; do 838 | if [ -d "GreatArtist$ARTIST_COUNT" ]; then 839 | cp -r "GreatArtist$ARTIST_COUNT" "OtherArtist$ARTIST_COUNT" &> /dev/null 840 | fi 841 | let ARTIST_COUNT=ARTIST_COUNT-1 842 | done 843 | 844 | echo "${GREEN}OK!${NC}" 845 | } 846 | 847 | # Check copied Artists 848 | function check_copied_artists { 849 | cd $PWD_DIR 850 | cd $DST_DIR 851 | echo -n "Checking copied Artists..." 852 | 853 | local ARTIST_COUNT=$TEST_SIZE 854 | local HAS_ERROR=0 855 | while [ $ARTIST_COUNT -gt 0 ]; do 856 | cd $PWD_DIR 857 | if [ -d "$DST_DIR/OtherArtist$ARTIST_COUNT" ]; then 858 | cd "$DST_DIR/OtherArtist$ARTIST_COUNT" 859 | local ALBUM_COUNT=$TEST_SIZE 860 | while [ $ALBUM_COUNT -gt 0 ]; do 861 | if [ -d "GreatAlbum$ALBUM_COUNT" ]; then 862 | cd "GreatAlbum$ALBUM_COUNT" 863 | local SONG_COUNT=$TEST_SIZE 864 | while [ $SONG_COUNT -gt 0 ]; do 865 | if [ -f "Song${SONG_COUNT}.mp3" ]; then 866 | if [ ! -s "Song${SONG_COUNT}.mp3" ]; then 867 | if [ $HAS_ERROR -eq 0 ] ; then 868 | echo "${RED}ERROR${NC}" 869 | fi 870 | HAS_ERROR=1 871 | echo "ERROR: File ${DST_DIR}/OtherArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 has 0 size" 872 | else 873 | check_tags Song${SONG_COUNT}.mp3 OtherArtist$ARTIST_COUNT GreatAlbum$ALBUM_COUNT Song${SONG_COUNT} 874 | if [ $? -ne 0 ] ; then 875 | if [ $HAS_ERROR -eq 0 ] ; then 876 | echo "${RED}ERROR${NC}" 877 | fi 878 | HAS_ERROR=1 879 | echo "ERROR: File ${DST_DIR}/OtherArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 tags not match" 880 | fi 881 | fi 882 | else 883 | if [ $HAS_ERROR -eq 0 ] ; then 884 | echo "${RED}ERROR${NC}" 885 | fi 886 | HAS_ERROR=1 887 | echo "ERROR: File ${DST_DIR}/OtherArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 not exists" 888 | fi 889 | let SONG_COUNT=SONG_COUNT-1 890 | done 891 | cd .. 892 | else 893 | if [ $HAS_ERROR -eq 0 ] ; then 894 | echo "${RED}ERROR${NC}" 895 | fi 896 | HAS_ERROR=1 897 | echo "ERROR: Directory ${DST_DIR}/OtherArtist${ARTIST_COUNT}/GreatAlbum${ALBUM_COUNT} not exists" 898 | fi 899 | let ALBUM_COUNT=ALBUM_COUNT-1 900 | done 901 | else 902 | if [ $HAS_ERROR -eq 0 ] ; then 903 | echo "${RED}ERROR${NC}" 904 | fi 905 | HAS_ERROR=1 906 | echo "ERROR: Directory ${DST_DIR}/OtherArtist${ARTIST_COUNT} not exists" 907 | fi 908 | let ARTIST_COUNT=ARTIST_COUNT-1 909 | done 910 | 911 | if [ $HAS_ERROR -eq 0 ] ; then 912 | echo "${GREEN}OK!${NC}" 913 | fi 914 | } 915 | 916 | # Delete copied artists 917 | function delete_artists { 918 | cd $PWD_DIR 919 | cd $DST_DIR 920 | echo -n "Deleting copied Artists..." 921 | 922 | local ARTIST_COUNT=$TEST_SIZE 923 | local HAS_ERROR=0 924 | while [ $ARTIST_COUNT -gt 0 ]; do 925 | cd $PWD_DIR 926 | if [ -d "$DST_DIR/OtherArtist$ARTIST_COUNT" ]; then 927 | rm -rf "$DST_DIR/OtherArtist$ARTIST_COUNT" &> /dev/null 928 | if [ $? -eq 0 ] ; then 929 | if [ -d "$DST_DIR/OtherArtist$ARTIST_COUNT" ]; then 930 | if [ $HAS_ERROR -eq 0 ] ; then 931 | echo "${RED}ERROR${NC}" 932 | fi 933 | HAS_ERROR=1 934 | echo "ERROR: Directory ${DST_DIR}/OtherArtist${ARTIST_COUNT} has not been deleted." 935 | fi 936 | else 937 | if [ $HAS_ERROR -eq 0 ] ; then 938 | echo "${RED}ERROR${NC}" 939 | fi 940 | HAS_ERROR=1 941 | echo "ERROR: Directory ${DST_DIR}/OtherArtist${ARTIST_COUNT} cannot be deleted." 942 | fi 943 | else 944 | if [ $HAS_ERROR -eq 0 ] ; then 945 | echo "${RED}ERROR${NC}" 946 | fi 947 | HAS_ERROR=1 948 | echo "ERROR: Directory ${DST_DIR}/OtherArtist${ARTIST_COUNT} not exists" 949 | fi 950 | let ARTIST_COUNT=ARTIST_COUNT-1 951 | done 952 | 953 | if [ $HAS_ERROR -eq 0 ] ; then 954 | echo "${GREEN}OK!${NC}" 955 | fi 956 | } 957 | 958 | # Move albums 959 | function move_albums { 960 | cd $PWD_DIR 961 | cd $DST_DIR 962 | echo -n "Moving Albums..." 963 | 964 | local HAS_ERROR=0 965 | if [ ! -d "DifferentArtist1" ]; then 966 | if [ $HAS_ERROR -eq 0 ] ; then 967 | echo "${RED}ERROR${NC}" 968 | fi 969 | HAS_ERROR=1 970 | echo "ERROR: Cannot find DifferentArtist1" 971 | return 972 | fi 973 | 974 | cd DifferentArtist1 975 | 976 | local ALBUM_COUNT=$TEST_SIZE 977 | while [ $ALBUM_COUNT -gt 0 ] ; do 978 | if [ -d "GreatAlbum$ALBUM_COUNT" ]; then 979 | mv "GreatAlbum$ALBUM_COUNT" "DifferentAlbum$ALBUM_COUNT" &> /dev/null 980 | fi 981 | let ALBUM_COUNT=ALBUM_COUNT-1 982 | done 983 | if [ $HAS_ERROR -eq 0 ] ; then 984 | echo "${GREEN}OK!${NC}" 985 | fi 986 | } 987 | 988 | # Check moved albums 989 | function check_moved_albums { 990 | cd $PWD_DIR 991 | cd $DST_DIR 992 | echo -n "Checking moved Albums..." 993 | local HAS_ERROR=0 994 | if [ ! -d "DifferentArtist1" ]; then 995 | if [ $HAS_ERROR -eq 0 ] ; then 996 | echo "${RED}ERROR${NC}" 997 | fi 998 | HAS_ERROR=1 999 | echo "ERROR: Cannot find DifferentArtist1" 1000 | return 1001 | fi 1002 | cd DifferentArtist1 1003 | 1004 | local ALBUM_COUNT=$TEST_SIZE 1005 | while [ $ALBUM_COUNT -gt 0 ]; do 1006 | if [ -d "DifferentAlbum$ALBUM_COUNT" ]; then 1007 | cd "DifferentAlbum$ALBUM_COUNT" 1008 | local SONG_COUNT=$TEST_SIZE 1009 | 1010 | while [ $SONG_COUNT -gt 0 ]; do 1011 | if [ -f "Song${SONG_COUNT}.mp3" ]; then 1012 | if [ ! -s "Song${SONG_COUNT}.mp3" ]; then 1013 | if [ $HAS_ERROR -eq 0 ] ; then 1014 | echo "${RED}ERROR${NC}" 1015 | fi 1016 | HAS_ERROR=1 1017 | echo "ERROR: File ${DST_DIR}/DifferentArtist1/DifferentAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 has 0 size" 1018 | else 1019 | check_tags Song${SONG_COUNT}.mp3 DifferentArtist1 DifferentAlbum$ALBUM_COUNT Song${SONG_COUNT} 1020 | if [ $? -ne 0 ] ; then 1021 | if [ $HAS_ERROR -eq 0 ] ; then 1022 | echo "${RED}ERROR${NC}" 1023 | fi 1024 | HAS_ERROR=1 1025 | echo "ERROR: File ${DST_DIR}/DifferentArtist1/DifferentAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 tags not match" 1026 | fi 1027 | fi 1028 | else 1029 | if [ $HAS_ERROR -eq 0 ] ; then 1030 | echo "${RED}ERROR${NC}" 1031 | fi 1032 | HAS_ERROR=1 1033 | echo "ERROR: File ${DST_DIR}/DifferentArtist1/DifferentAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 not exists" 1034 | fi 1035 | let SONG_COUNT=SONG_COUNT-1 1036 | done 1037 | cd .. 1038 | else 1039 | if [ $HAS_ERROR -eq 0 ] ; then 1040 | echo "${RED}ERROR${NC}" 1041 | fi 1042 | HAS_ERROR=1 1043 | echo "ERROR: Directory ${DST_DIR}/DifferentArtist1/DifferentAlbum${ALBUM_COUNT} not exists" 1044 | fi 1045 | let ALBUM_COUNT=ALBUM_COUNT-1 1046 | done 1047 | 1048 | if [ $HAS_ERROR -eq 0 ] ; then 1049 | echo "${GREEN}OK!${NC}" 1050 | fi 1051 | } 1052 | 1053 | # Copy albums around 1054 | function copy_albums { 1055 | cd $PWD_DIR 1056 | cd $DST_DIR 1057 | echo -n "Copying Albums around..." 1058 | 1059 | local HAS_ERROR=0 1060 | if [ ! -d "GreatArtist1" ]; then 1061 | if [ $HAS_ERROR -eq 0 ] ; then 1062 | echo "${RED}ERROR${NC}" 1063 | fi 1064 | HAS_ERROR=1 1065 | echo "ERROR: Cannot find GreatArtist1" 1066 | return 1067 | fi 1068 | 1069 | cd GreatArtist1 1070 | 1071 | local ALBUM_COUNT=$TEST_SIZE 1072 | while [ $ALBUM_COUNT -gt 0 ] ; do 1073 | if [ -d "GreatAlbum$ALBUM_COUNT" ]; then 1074 | cp -r "GreatAlbum$ALBUM_COUNT" "OtherAlbum$ALBUM_COUNT" &> /dev/null 1075 | fi 1076 | let ALBUM_COUNT=ALBUM_COUNT-1 1077 | done 1078 | if [ $HAS_ERROR -eq 0 ] ; then 1079 | echo "${GREEN}OK!${NC}" 1080 | fi 1081 | } 1082 | 1083 | # Check copied albums 1084 | function check_copied_albums { 1085 | cd $PWD_DIR 1086 | cd $DST_DIR 1087 | echo -n "Checking copied Albums..." 1088 | local HAS_ERROR=0 1089 | if [ ! -d "GreatArtist1" ]; then 1090 | if [ $HAS_ERROR -eq 0 ] ; then 1091 | echo "${RED}ERROR${NC}" 1092 | fi 1093 | HAS_ERROR=1 1094 | echo "ERROR: Cannot find GreatArtist1" 1095 | return 1096 | fi 1097 | cd GreatArtist1 1098 | 1099 | local ALBUM_COUNT=$TEST_SIZE 1100 | while [ $ALBUM_COUNT -gt 0 ]; do 1101 | if [ -d "OtherAlbum$ALBUM_COUNT" ]; then 1102 | cd "OtherAlbum$ALBUM_COUNT" 1103 | local SONG_COUNT=$TEST_SIZE 1104 | 1105 | while [ $SONG_COUNT -gt 0 ]; do 1106 | if [ -f "Song${SONG_COUNT}.mp3" ]; then 1107 | if [ ! -s "Song${SONG_COUNT}.mp3" ]; then 1108 | if [ $HAS_ERROR -eq 0 ] ; then 1109 | echo "${RED}ERROR${NC}" 1110 | fi 1111 | HAS_ERROR=1 1112 | echo "ERROR: File ${DST_DIR}/GreatArtist1/OtherAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 has 0 size" 1113 | else 1114 | check_tags Song${SONG_COUNT}.mp3 GreatArtist1 OtherAlbum$ALBUM_COUNT Song${SONG_COUNT} 1115 | if [ $? -ne 0 ] ; then 1116 | if [ $HAS_ERROR -eq 0 ] ; then 1117 | echo "${RED}ERROR${NC}" 1118 | fi 1119 | HAS_ERROR=1 1120 | echo "ERROR: File ${DST_DIR}/GreatArtist1/OtherAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 tags not match" 1121 | fi 1122 | fi 1123 | else 1124 | if [ $HAS_ERROR -eq 0 ] ; then 1125 | echo "${RED}ERROR${NC}" 1126 | fi 1127 | HAS_ERROR=1 1128 | echo "ERROR: File ${DST_DIR}/GreatArtist1/OtherAlbum${ALBUM_COUNT}/Song${SONG_COUNT}.mp3 not exists" 1129 | fi 1130 | let SONG_COUNT=SONG_COUNT-1 1131 | done 1132 | cd .. 1133 | else 1134 | if [ $HAS_ERROR -eq 0 ] ; then 1135 | echo "${RED}ERROR${NC}" 1136 | fi 1137 | HAS_ERROR=1 1138 | echo "ERROR: Directory ${DST_DIR}/GreatArtist1/OtherAlbum${ALBUM_COUNT} not exists" 1139 | fi 1140 | let ALBUM_COUNT=ALBUM_COUNT-1 1141 | done 1142 | 1143 | if [ $HAS_ERROR -eq 0 ] ; then 1144 | echo "${GREEN}OK!${NC}" 1145 | fi 1146 | } 1147 | 1148 | # Delete copied albums 1149 | function delete_albums { 1150 | cd $PWD_DIR 1151 | cd $DST_DIR 1152 | echo -n "Deleting copied Albums..." 1153 | local HAS_ERROR=0 1154 | if [ ! -d "GreatArtist1" ]; then 1155 | if [ $HAS_ERROR -eq 0 ] ; then 1156 | echo "${RED}ERROR${NC}" 1157 | fi 1158 | HAS_ERROR=1 1159 | echo "ERROR: Cannot find GreatArtist1" 1160 | return 1161 | fi 1162 | cd GreatArtist1 1163 | 1164 | local ALBUM_COUNT=$TEST_SIZE 1165 | while [ $ALBUM_COUNT -gt 0 ]; do 1166 | if [ -d "OtherAlbum$ALBUM_COUNT" ]; then 1167 | cd "OtherAlbum$ALBUM_COUNT" 1168 | 1169 | rm -rf "OtherAlbum$ALBUM_COUNT" &> /dev/null 1170 | if [ $? -eq 0 ] ; then 1171 | if [ -d "OtherAlbum$ALBUM_COUNT" ]; then 1172 | if [ $HAS_ERROR -eq 0 ] ; then 1173 | echo "${RED}ERROR${NC}" 1174 | fi 1175 | HAS_ERROR=1 1176 | echo "ERROR: Directory ${DST_DIR}/GreatArtist1/OtherAlbum${ALBUM_COUNT} has not been deleted." 1177 | fi 1178 | else 1179 | if [ $HAS_ERROR -eq 0 ] ; then 1180 | echo "${RED}ERROR${NC}" 1181 | fi 1182 | HAS_ERROR=1 1183 | echo "ERROR: Directory ${DST_DIR}/GreatArtist1/OtherAlbum${ALBUM_COUNT} cannot be deleted." 1184 | fi 1185 | cd .. 1186 | else 1187 | if [ $HAS_ERROR -eq 0 ] ; then 1188 | echo "${RED}ERROR${NC}" 1189 | fi 1190 | HAS_ERROR=1 1191 | echo "ERROR: Directory ${DST_DIR}/GreatArtist1/OtherAlbum${ALBUM_COUNT} not exists" 1192 | fi 1193 | let ALBUM_COUNT=ALBUM_COUNT-1 1194 | done 1195 | 1196 | if [ $HAS_ERROR -eq 0 ] ; then 1197 | echo "${GREEN}OK!${NC}" 1198 | fi 1199 | } 1200 | 1201 | # Move songs 1202 | function move_songs { 1203 | cd $PWD_DIR 1204 | cd $DST_DIR 1205 | echo -n "Moving Songs..." 1206 | 1207 | local HAS_ERROR=0 1208 | if [ ! -d "DifferentArtist1" ]; then 1209 | if [ $HAS_ERROR -eq 0 ] ; then 1210 | echo "${RED}ERROR${NC}" 1211 | fi 1212 | HAS_ERROR=1 1213 | echo "ERROR: Cannot find DifferentArtist1" 1214 | return 1215 | fi 1216 | 1217 | cd DifferentArtist1 1218 | 1219 | if [ ! -d "DifferentAlbum1" ]; then 1220 | if [ $HAS_ERROR -eq 0 ] ; then 1221 | echo "${RED}ERROR${NC}" 1222 | fi 1223 | HAS_ERROR=1 1224 | echo "ERROR: Cannot find DifferentAlbum1" 1225 | return 1226 | fi 1227 | 1228 | cd DifferentAlbum1 1229 | local SONG_COUNT=$TEST_SIZE 1230 | while [ $SONG_COUNT -gt 0 ] ; do 1231 | if [ -f "Song$SONG_COUNT.mp3" ]; then 1232 | cp "Song$SONG_COUNT.mp3" "DifferentSong$SONG_COUNT.mp3" &> /dev/null 1233 | fi 1234 | let SONG_COUNT=SONG_COUNT-1 1235 | done 1236 | if [ $HAS_ERROR -eq 0 ] ; then 1237 | echo "${GREEN}OK!${NC}" 1238 | fi 1239 | } 1240 | 1241 | # Check moved songs 1242 | function check_moved_songs { 1243 | cd $PWD_DIR 1244 | cd $DST_DIR 1245 | echo -n "Checking moved Songs..." 1246 | local HAS_ERROR=0 1247 | if [ ! -d "DifferentArtist1" ]; then 1248 | if [ $HAS_ERROR -eq 0 ] ; then 1249 | echo "${RED}ERROR${NC}" 1250 | fi 1251 | HAS_ERROR=1 1252 | echo "ERROR: Cannot find DifferentArtist1" 1253 | return 1254 | fi 1255 | cd DifferentArtist1 1256 | 1257 | if [ ! -d "DifferentAlbum1" ]; then 1258 | if [ $HAS_ERROR -eq 0 ] ; then 1259 | echo "${RED}ERROR${NC}" 1260 | fi 1261 | HAS_ERROR=1 1262 | echo "ERROR: Cannot find DifferentAlbum1" 1263 | return 1264 | fi 1265 | cd DifferentAlbum1 1266 | 1267 | local SONG_COUNT=$TEST_SIZE 1268 | 1269 | while [ $SONG_COUNT -gt 0 ]; do 1270 | if [ -f "DifferentSong${SONG_COUNT}.mp3" ]; then 1271 | if [ ! -s "DifferentSong${SONG_COUNT}.mp3" ]; then 1272 | if [ $HAS_ERROR -eq 0 ] ; then 1273 | echo "${RED}ERROR${NC}" 1274 | fi 1275 | HAS_ERROR=1 1276 | echo "ERROR: File ${DST_DIR}/DifferentArtist1/DifferentAlbum1/DifferentSong${SONG_COUNT}.mp3 has 0 size" 1277 | else 1278 | check_tags DifferentSong${SONG_COUNT}.mp3 DifferentArtist1 DifferentAlbum1 DifferentSong${SONG_COUNT} 1279 | if [ $? -ne 0 ] ; then 1280 | if [ $HAS_ERROR -eq 0 ] ; then 1281 | echo "${RED}ERROR${NC}" 1282 | fi 1283 | HAS_ERROR=1 1284 | echo "ERROR: File ${DST_DIR}/DifferentArtist1/DifferentAlbum1/DifferentSong${SONG_COUNT}.mp3 tags not match" 1285 | fi 1286 | fi 1287 | else 1288 | if [ $HAS_ERROR -eq 0 ] ; then 1289 | echo "${RED}ERROR${NC}" 1290 | fi 1291 | HAS_ERROR=1 1292 | echo "ERROR: File ${DST_DIR}/DifferentArtist1/DifferentAlbum1/DifferentSong${SONG_COUNT}.mp3 not exists" 1293 | fi 1294 | let SONG_COUNT=SONG_COUNT-1 1295 | done 1296 | 1297 | if [ $HAS_ERROR -eq 0 ] ; then 1298 | echo "${GREEN}OK!${NC}" 1299 | fi 1300 | } 1301 | 1302 | # Copy songs around 1303 | function copy_songs { 1304 | cd $PWD_DIR 1305 | cd $DST_DIR 1306 | echo -n "Copying Songs around..." 1307 | 1308 | local HAS_ERROR=0 1309 | if [ ! -d "GreatArtist1" ]; then 1310 | if [ $HAS_ERROR -eq 0 ] ; then 1311 | echo "${RED}ERROR${NC}" 1312 | fi 1313 | HAS_ERROR=1 1314 | echo "ERROR: Cannot find GreatArtist1" 1315 | return 1316 | fi 1317 | 1318 | cd GreatArtist1 1319 | 1320 | if [ ! -d "GreatAlbum1" ]; then 1321 | if [ $HAS_ERROR -eq 0 ] ; then 1322 | echo "${RED}ERROR${NC}" 1323 | fi 1324 | HAS_ERROR=1 1325 | echo "ERROR: Cannot find GreatAlbum1" 1326 | return 1327 | fi 1328 | 1329 | cd GreatAlbum1 1330 | local SONG_COUNT=$TEST_SIZE 1331 | while [ $SONG_COUNT -gt 0 ] ; do 1332 | if [ -f "Song$SONG_COUNT.mp3" ]; then 1333 | cp "Song$SONG_COUNT.mp3" "OtherSong$SONG_COUNT.mp3" &> /dev/null 1334 | fi 1335 | let SONG_COUNT=SONG_COUNT-1 1336 | done 1337 | if [ $HAS_ERROR -eq 0 ] ; then 1338 | echo "${GREEN}OK!${NC}" 1339 | fi 1340 | } 1341 | 1342 | # Check copied songs 1343 | function check_copied_songs { 1344 | cd $PWD_DIR 1345 | cd $DST_DIR 1346 | echo -n "Checking copied Songs..." 1347 | local HAS_ERROR=0 1348 | if [ ! -d "GreatArtist1" ]; then 1349 | if [ $HAS_ERROR -eq 0 ] ; then 1350 | echo "${RED}ERROR${NC}" 1351 | fi 1352 | HAS_ERROR=1 1353 | echo "ERROR: Cannot find GreatArtist1" 1354 | return 1355 | fi 1356 | cd GreatArtist1 1357 | 1358 | if [ ! -d "GreatAlbum1" ]; then 1359 | if [ $HAS_ERROR -eq 0 ] ; then 1360 | echo "${RED}ERROR${NC}" 1361 | fi 1362 | HAS_ERROR=1 1363 | echo "ERROR: Cannot find GreatAlbum1" 1364 | return 1365 | fi 1366 | cd GreatAlbum1 1367 | 1368 | local SONG_COUNT=$TEST_SIZE 1369 | 1370 | while [ $SONG_COUNT -gt 0 ]; do 1371 | if [ -f "OtherSong${SONG_COUNT}.mp3" ]; then 1372 | if [ ! -s "OtherSong${SONG_COUNT}.mp3" ]; then 1373 | if [ $HAS_ERROR -eq 0 ] ; then 1374 | echo "${RED}ERROR${NC}" 1375 | fi 1376 | HAS_ERROR=1 1377 | echo "ERROR: File ${DST_DIR}/GreatArtist1/GreatAlbum1/OtherSong${SONG_COUNT}.mp3 has 0 size" 1378 | else 1379 | check_tags OtherSong${SONG_COUNT}.mp3 GreatArtist1 GreatAlbum1 OtherSong${SONG_COUNT} 1380 | if [ $? -ne 0 ] ; then 1381 | if [ $HAS_ERROR -eq 0 ] ; then 1382 | echo "${RED}ERROR${NC}" 1383 | fi 1384 | HAS_ERROR=1 1385 | echo "ERROR: File ${DST_DIR}/GreatArtist1/GreatAlbum1/OtherSong${SONG_COUNT}.mp3 tags not match" 1386 | fi 1387 | fi 1388 | else 1389 | if [ $HAS_ERROR -eq 0 ] ; then 1390 | echo "${RED}ERROR${NC}" 1391 | fi 1392 | HAS_ERROR=1 1393 | echo "ERROR: File ${DST_DIR}/GreatArtist1/GreatAlbum1/OtherSong${SONG_COUNT}.mp3 not exists" 1394 | fi 1395 | let SONG_COUNT=SONG_COUNT-1 1396 | done 1397 | 1398 | if [ $HAS_ERROR -eq 0 ] ; then 1399 | echo "${GREEN}OK!${NC}" 1400 | fi 1401 | } 1402 | 1403 | # Delete songs 1404 | function delete_songs { 1405 | cd $PWD_DIR 1406 | cd $DST_DIR 1407 | echo -n "Deleting copied Songs..." 1408 | local HAS_ERROR=0 1409 | if [ ! -d "GreatArtist1" ]; then 1410 | if [ $HAS_ERROR -eq 0 ] ; then 1411 | echo "${RED}ERROR${NC}" 1412 | fi 1413 | HAS_ERROR=1 1414 | echo "ERROR: Cannot find GreatArtist1" 1415 | return 1416 | fi 1417 | cd GreatArtist1 1418 | 1419 | if [ ! -d "GreatAlbum1" ]; then 1420 | if [ $HAS_ERROR -eq 0 ] ; then 1421 | echo "${RED}ERROR${NC}" 1422 | fi 1423 | HAS_ERROR=1 1424 | echo "ERROR: Cannot find GreatAlbum1" 1425 | return 1426 | fi 1427 | cd GreatAlbum1 1428 | 1429 | local SONG_COUNT=$TEST_SIZE 1430 | 1431 | while [ $SONG_COUNT -gt 0 ]; do 1432 | if [ -f "OtherSong${SONG_COUNT}.mp3" ]; then 1433 | rm -f OtherSong${SONG_COUNT}.mp3 &> /dev/null 1434 | if [ $? -ne 0 ] ; then 1435 | if [ $HAS_ERROR -eq 0 ] ; then 1436 | echo "${RED}ERROR${NC}" 1437 | fi 1438 | HAS_ERROR=1 1439 | echo "ERROR: File ${DST_DIR}/GreatArtist1/GreatAlbum1/OtherSong${SONG_COUNT}.mp3 cannot be deleted." 1440 | else 1441 | if [ -f "OtherSong${SONG_COUNT}.mp3" ]; then 1442 | if [ $HAS_ERROR -eq 0 ] ; then 1443 | echo "${RED}ERROR${NC}" 1444 | fi 1445 | HAS_ERROR=1 1446 | echo "ERROR: File ${DST_DIR}/GreatArtist1/GreatAlbum1/OtherSong${SONG_COUNT}.mp3 has not been deleted." 1447 | fi 1448 | fi 1449 | else 1450 | if [ $HAS_ERROR -eq 0 ] ; then 1451 | echo "${RED}ERROR${NC}" 1452 | fi 1453 | HAS_ERROR=1 1454 | echo "ERROR: File ${DST_DIR}/GreatArtist1/GreatAlbum1/OtherSong${SONG_COUNT}.mp3 not exists" 1455 | fi 1456 | let SONG_COUNT=SONG_COUNT-1 1457 | done 1458 | 1459 | if [ $HAS_ERROR -eq 0 ] ; then 1460 | echo "${GREEN}OK!${NC}" 1461 | fi 1462 | } 1463 | 1464 | # Pre-Mount function 1465 | function create_dirs { 1466 | cd $PWD_DIR 1467 | echo -n "Creating dirs..." 1468 | mkdir $SRC_DIR $DST_DIR &> /dev/null 1469 | if [ $? -eq 0 ] ; then 1470 | echo "${GREEN}OK!${NC}" 1471 | else 1472 | echo "${RED}ERROR${NC}" 1473 | fi 1474 | } 1475 | 1476 | # Mount FS function 1477 | function mount_muli { 1478 | cd $PWD_DIR 1479 | echo -n "Mounting MuLi filesystem..." 1480 | # Run MuLi in the background 1481 | $MULI_X $SRC_DIR $DST_DIR &> muli.log & 1482 | while [ ! -d "$DST_DIR/drop" ]; do 1483 | sleep 1 1484 | done 1485 | echo "${GREEN}OK!${NC}" 1486 | } 1487 | 1488 | # Umount FS function 1489 | function umount_muli { 1490 | cd $PWD_DIR 1491 | echo -n "Umounting MuLi filesystem..." 1492 | # Umount the destination directory 1493 | umount $DST_DIR 1494 | if [ $? -eq 0 ] ; then 1495 | echo "${GREEN}OK!${NC}" 1496 | else 1497 | echo "${RED}ERROR${NC}" 1498 | fi 1499 | } 1500 | 1501 | # Clean everything up 1502 | function clean_up { 1503 | cd $PWD_DIR 1504 | echo -n "Cleaning everything up..." 1505 | # Delete the SRC and DST folders 1506 | rm -rf $SRC_DIR $DST_DIR muli.db 1507 | if [ $? -eq 0 ] ; then 1508 | echo "${GREEN}OK!${NC}" 1509 | else 1510 | echo "${RED}ERROR${NC}" 1511 | fi 1512 | } 1513 | 1514 | # Perform tests 1515 | create_dirs 1516 | create_fake 1517 | create_empty 1518 | create_special 1519 | mount_muli 1520 | #check_fake 1521 | #check_empty 1522 | #check_special 1523 | #copy_artists 1524 | #check_copied_artists 1525 | #sleep 3 1526 | #copy_albums 1527 | #sleep 3 1528 | #check_copied_albums 1529 | #copy_songs 1530 | #sleep 3 1531 | #check_copied_songs 1532 | #delete_songs 1533 | #delete_albums 1534 | #delete_artists 1535 | #move_artists 1536 | #sleep 3 1537 | #check_moved_artists 1538 | #move_albums 1539 | #sleep 3 1540 | #check_moved_albums 1541 | #move_songs 1542 | #sleep 3 1543 | #check_moved_songs 1544 | #mkdirs 1545 | #check_mkdirs 1546 | #drop_files 1547 | #check_fake 1548 | playlist_mkdirs 1549 | sleep 5 1550 | check_playlists_mkdirs 1551 | move_playlists_dirs 1552 | sleep 3 1553 | check_moved_playlists_dirs 1554 | #move_playlists_files 1555 | #sleep 3 1556 | #check_moved_playlists_files 1557 | #umount_muli 1558 | #clean_up 1559 | 1560 | 1561 | --------------------------------------------------------------------------------