├── .gitignore ├── README.md ├── Shellify └── Shellify │ ├── Player.swift │ ├── ShellifyError.swift │ ├── Song.swift │ └── main.swift └── img-resources └── catJAM.gif /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,objective-c,cocoapods,carthage,swiftpackagemanager 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift,objective-c,cocoapods,carthage,swiftpackagemanager 4 | 5 | #Ignoring song files due to copyright and piracy compliance 6 | /resources 7 | .m4a 8 | 9 | #MacOSX File System configuration 10 | .DS_Store 11 | 12 | ### Carthage ### 13 | # Carthage 14 | # 15 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 16 | # Carthage/Checkouts 17 | 18 | Carthage/Build 19 | 20 | ### CocoaPods ### 21 | ## CocoaPods GitIgnore Template 22 | 23 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 24 | # - Also handy if you have a large number of dependant pods 25 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 26 | Pods/ 27 | 28 | ### Objective-C ### 29 | # Xcode 30 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 31 | 32 | ## User settings 33 | xcuserdata/ 34 | 35 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 36 | *.xcscmblueprint 37 | *.xccheckout 38 | 39 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 40 | build/ 41 | DerivedData/ 42 | *.moved-aside 43 | *.pbxuser 44 | !default.pbxuser 45 | *.mode1v3 46 | !default.mode1v3 47 | *.mode2v3 48 | !default.mode2v3 49 | *.perspectivev3 50 | !default.perspectivev3 51 | 52 | ## Obj-C/Swift specific 53 | *.hmap 54 | 55 | ## App packaging 56 | *.ipa 57 | *.dSYM.zip 58 | *.dSYM 59 | 60 | # CocoaPods 61 | # We recommend against adding the Pods directory to your .gitignore. However 62 | # you should judge for yourself, the pros and cons are mentioned at: 63 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 64 | # Pods/ 65 | # Add this line if you want to avoid checking in source code from the Xcode workspace 66 | # *.xcworkspace 67 | 68 | # Carthage 69 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 70 | # Carthage/Checkouts 71 | 72 | Carthage/Build/ 73 | 74 | # fastlane 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # After new code Injection tools there's a generated folder /iOSInjectionProject 87 | # https://github.com/johnno1962/injectionforxcode 88 | 89 | iOSInjectionProject/ 90 | 91 | ### Objective-C Patch ### 92 | 93 | ### Swift ### 94 | # Xcode 95 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 96 | 97 | 98 | 99 | 100 | 101 | 102 | ## Playgrounds 103 | timeline.xctimeline 104 | playground.xcworkspace 105 | 106 | # Swift Package Manager 107 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 108 | # Packages/ 109 | # Package.pins 110 | # Package.resolved 111 | # *.xcodeproj 112 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 113 | # hence it is not needed unless you have added a package configuration file to your project 114 | # .swiftpm 115 | 116 | .build/ 117 | 118 | # CocoaPods 119 | # We recommend against adding the Pods directory to your .gitignore. However 120 | # you should judge for yourself, the pros and cons are mentioned at: 121 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 122 | # Pods/ 123 | # Add this line if you want to avoid checking in source code from the Xcode workspace 124 | # *.xcworkspace 125 | 126 | # Carthage 127 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 128 | # Carthage/Checkouts 129 | 130 | 131 | # Accio dependency management 132 | Dependencies/ 133 | .accio/ 134 | 135 | # fastlane 136 | # It is recommended to not store the screenshots in the git repo. 137 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 138 | # For more information about the recommended setup visit: 139 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 140 | 141 | 142 | # Code Injection 143 | # After new code Injection tools there's a generated folder /iOSInjectionProject 144 | # https://github.com/johnno1962/injectionforxcode 145 | 146 | 147 | ### SwiftPackageManager ### 148 | Packages 149 | xcuserdata 150 | *.xcodeproj 151 | 152 | 153 | ### Xcode ### 154 | # Xcode 155 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 156 | 157 | 158 | 159 | 160 | ## Gcc Patch 161 | /*.gcno 162 | 163 | ### Xcode Patch ### 164 | *.xcodeproj/* 165 | !*.xcodeproj/project.pbxproj 166 | !*.xcodeproj/xcshareddata/ 167 | !*.xcworkspace/contents.xcworkspacedata 168 | **/xcshareddata/WorkspaceSettings.xcsettings 169 | 170 | # End of https://www.toptal.com/developers/gitignore/api/xcode,swift,objective-c,cocoapods,carthage,swiftpackagemanager -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Welcome to Shellify! 5 | 6 | 7 | ### What is Shellify? 8 | Shellify is a command-line tool based application for local reproduction of songs. If your internet is down or not yet installed, but still you need to catJAM those beats, Shellify is the way to go! 9 | 10 | ### How Shellify works 11 | Here in this repository lays the source code for our application, therefore we are still working on ways to make it easier to create the executable and just run. But assuming you already have [Xcode](https://developer.apple.com/xcode/) installed on your OSx (Mac, MacBook) just simply create an Xcode project and import the source code inside it. Once imported, just execute as a normal command-line program. 12 | 13 | ### Command list 14 | - **play** song-name-here | plays the song you've inserted 15 | - **skip** | skips to next song. If last, skips back to the first 16 | - **pause** | pauses the song 17 | - **exit** | exits the program 18 | 19 | Thanks for stopping by! 🥳 20 | Authors: [Me](https://github.com/MarinaFX) and [Diego](https://github.com/DiegoHSO) 21 | -------------------------------------------------------------------------------- /Shellify/Shellify/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // Shellify 4 | // 5 | // Created by Marina De Pazzi and Diego Henrique on 17/03/21. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | class Player { 12 | var player: AVAudioPlayer? 13 | var songList: [Song] 14 | 15 | init () { 16 | songList = [] 17 | } 18 | 19 | func loadPlaylist(userPath: String) throws { 20 | let searchPath = FileManager.default 21 | do { 22 | let songsURLs = try searchPath.contentsOfDirectory(atPath: userPath) 23 | for paths in songsURLs { 24 | try addSong(songName: userPath + "/" + paths) 25 | } 26 | } catch (ShellifyError.UnknownPath) { 27 | throw ShellifyError.UnknownPath 28 | } 29 | } 30 | 31 | 32 | func addSong(songName : String) throws { 33 | let searchPath = FileManager.default 34 | 35 | if searchPath.fileExists(atPath: songName) { 36 | var newSong = Song(name: "", artist: "", albumName: "", duration: 0, URL: songName) 37 | let urlString = URL(fileURLWithPath: songName) 38 | let avpItem = AVPlayerItem(url: urlString) 39 | let commonMetaData = avpItem.asset.commonMetadata 40 | for item in commonMetaData { 41 | if item.commonKey?.rawValue == "title" { 42 | guard let songTitle = item.stringValue else { 43 | throw ShellifyError.SongParametrizationFailed 44 | } 45 | newSong.name = songTitle 46 | } 47 | if item.commonKey?.rawValue == "artist" { 48 | guard let songArtist = item.stringValue else { 49 | throw ShellifyError.SongParametrizationFailed 50 | } 51 | newSong.artist = songArtist 52 | } 53 | if item.commonKey?.rawValue == "albumName" { 54 | guard let songAlbum = item.stringValue else { 55 | throw ShellifyError.SongParametrizationFailed 56 | } 57 | newSong.albumName = songAlbum 58 | } 59 | } 60 | do { 61 | let player = try AVAudioPlayer(contentsOf: urlString) 62 | newSong.duration = player.duration 63 | } catch (ShellifyError.SongParametrizationFailed) { 64 | throw ShellifyError.SongParametrizationFailed 65 | } 66 | songList.append(newSong) 67 | } 68 | else { 69 | throw ShellifyError.SongFileNotFound 70 | } 71 | } 72 | 73 | func playSong(songName: String?) throws { 74 | if let player = player, player.isPlaying { 75 | player.stop() 76 | try playSong(songName: songName) 77 | } 78 | else { 79 | 80 | do { 81 | guard let songPath = try isSongValid(songName: songName) else { 82 | throw ShellifyError.SongNotFound 83 | } 84 | 85 | let searchPath = FileManager.default 86 | 87 | if searchPath.fileExists(atPath: songPath) { 88 | let urlString = URL(fileURLWithPath: songPath) 89 | do { 90 | player = try AVAudioPlayer(contentsOf: urlString) 91 | 92 | guard let player = player else { 93 | throw ShellifyError.PlaybackError 94 | } 95 | player.prepareToPlay() 96 | player.play() 97 | return 98 | } catch (ShellifyError.SongFileNotFound) { 99 | throw ShellifyError.SongFileNotFound 100 | } 101 | } 102 | else { 103 | throw ShellifyError.SongNotFound 104 | } 105 | } 106 | } 107 | } 108 | 109 | func continueReproduction() throws { 110 | do { 111 | if try !isPlaying() { 112 | player?.play() 113 | } 114 | } catch (ShellifyError.PlaybackError){ 115 | throw ShellifyError.PlaybackError 116 | } 117 | } 118 | 119 | func skipSong() throws -> Song? { 120 | do { 121 | if try isPlaying() { 122 | player?.stop() 123 | for i in songList.indices { 124 | let stringURL = player!.url!.relativePath 125 | if stringURL == songList[i].URL { 126 | if i == songList.count-1 { 127 | let urlString = URL(fileURLWithPath: songList[0].URL) 128 | player = try AVAudioPlayer(contentsOf: urlString) 129 | guard let player = player else { 130 | throw ShellifyError.PlaybackError 131 | } 132 | player.prepareToPlay() 133 | player.play() 134 | return songList[0] 135 | } 136 | else { 137 | let urlString = URL(fileURLWithPath: songList[i+1].URL) 138 | player = try AVAudioPlayer(contentsOf: urlString) 139 | guard let player = player else { 140 | throw ShellifyError.PlaybackError 141 | } 142 | player.prepareToPlay() 143 | player.play() 144 | return songList[i+1] 145 | } 146 | } 147 | } 148 | return nil 149 | } 150 | return nil 151 | } catch (ShellifyError.PlaybackError){ 152 | throw ShellifyError.PlaybackError 153 | } 154 | } 155 | 156 | func pauseSong() throws { 157 | do { 158 | if try isPlaying() { 159 | player?.stop() 160 | } 161 | } catch (ShellifyError.PlaybackError){ 162 | throw ShellifyError.PlaybackError 163 | } 164 | } 165 | 166 | 167 | func isPlaying() throws -> Bool { 168 | if let unwrappedPlayer: AVAudioPlayer = player { 169 | return unwrappedPlayer.isPlaying 170 | } 171 | throw ShellifyError.PlaybackError 172 | } 173 | 174 | func isSongValid(songName: String?) throws -> String? { 175 | if let unwrappedName: String = songName { 176 | for s in songList { 177 | if s.name.localizedCaseInsensitiveContains(unwrappedName){ 178 | return s.URL 179 | } 180 | } 181 | return nil 182 | } 183 | 184 | throw ShellifyError.InvalidSongName 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Shellify/Shellify/ShellifyError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SongNotFoundError.swift 3 | // Shellify 4 | // 5 | // Created by Marina De Pazzi and Diego Henrique on 17/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ShellifyError: Error { 11 | case SongNotFound 12 | case InvalidSongName 13 | case PlaybackError 14 | case SongFileNotFound 15 | case SongParametrizationFailed 16 | case UnknownPath 17 | } 18 | -------------------------------------------------------------------------------- /Shellify/Shellify/Song.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Song.swift 3 | // Shellify 4 | // 5 | // Created by Marina De Pazzi on 17/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Song { 11 | var name: String 12 | var artist: String 13 | var albumName: String 14 | var duration: TimeInterval 15 | var URL : String 16 | } 17 | -------------------------------------------------------------------------------- /Shellify/Shellify/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Shellify 4 | // 5 | // Created by Marina De Pazzi and Diego Henrique on 16/03/21. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | func startProgram(){ 12 | var str = " _____ _ _ _ _ __\n" 13 | str += " / ____|| | | || |(_) / _|\n" 14 | str += " | (___ | |__ ___ | || | _ | |_ _ _\n" 15 | str += " \\___ \\ | '_ \\ / _ \\| || || || _|| | | |\n" 16 | str += " ____) || | | || __/| || || || | | |_| |\n" 17 | str += " |_____/ |_| |_| \\___||_||_||_||_| \\__, |\n" 18 | str += " __/ |\n" 19 | str += " |___/\n" 20 | str += " \n" 21 | str += "Please paste the directory where your songs are:" 22 | 23 | print(str) 24 | } 25 | 26 | func endProgram() { 27 | var str = " _____ _ _ _ _ __\n" 28 | str += " / ____|| | | || |(_) / _|\n" 29 | str += " | (___ | |__ ___ | || | _ | |_ _ _\n" 30 | str += " \\___ \\ | '_ \\ / _ \\| || || || _|| | | |\n" 31 | str += " ____) || | | || __/| || || || | | |_| |\n" 32 | str += " |_____/ |_| |_| \\___||_||_||_||_| \\__, |\n" 33 | str += " __/ |\n" 34 | str += " |___/\n" 35 | 36 | print(str)} 37 | 38 | func playMusic(song: Song) { 39 | let formatter = DateComponentsFormatter() 40 | formatter.unitsStyle = .positional 41 | formatter.allowedUnits = [ .minute, .second ] 42 | formatter.zeroFormattingBehavior = [ .pad ] 43 | 44 | print(" ___________________________________________________________") 45 | print("| ______________________________________________________ |") 46 | print("| / .--------------------------------------------------. \\ |") 47 | if song.name.count < 37 { 48 | print("| | | /\\ : \(song.name)", terminator:"") 49 | for _ in song.name.count ... 37 { 50 | print(" ", terminator:"") 51 | } 52 | print("\(formatter.string(from: song.duration)!) | | |") 53 | } 54 | if song.artist.count < 35 { 55 | print("| | |/--\\: \(song.artist) ", terminator:"") 56 | for _ in song.artist.count ... 35 { 57 | print(".", terminator:"") 58 | } 59 | print(" NR [ ]| | |") 60 | } 61 | print("| | `--------------------------------------------------' | |") 62 | print("| | //-\\\\ | | //-\\\\ | |") 63 | print("| | ||( )|| |_________| ||( )|| | |") 64 | print("| | \\\\-// :....:....: \\\\-// | |") 65 | print("| | _ _ ._ _ _ .__|_ _.._ _ | |") 66 | print("| | (_(_)| |(_(/_| |_(_||_)(/_ | |") 67 | print("| | | | |") 68 | print("| `_____ ____________________________________ ____ _______' |") 69 | print("| / [] [] \\ |") 70 | print("| / () () \\ |") 71 | print("!_____/_____________________________________________\\_______!") 72 | } 73 | 74 | 75 | 76 | func readUserInput() -> String { 77 | if let unrwrappedAnswer: String = readLine() { 78 | return unrwrappedAnswer 79 | } 80 | return "An error occurred while reading the user input" 81 | } 82 | 83 | func showUserLibrary(library : [Song]){ 84 | var totalDuration : TimeInterval = 0 85 | var calendar = Calendar.current 86 | calendar.locale = Locale(identifier: "en-UK") 87 | let formatter = DateComponentsFormatter() 88 | formatter.calendar = calendar 89 | formatter.unitsStyle = .abbreviated 90 | formatter.allowedUnits = [ .minute, .second ] 91 | formatter.zeroFormattingBehavior = [ .pad ] 92 | 93 | for song in library { 94 | totalDuration += song.duration 95 | } 96 | print(" ____________") 97 | print(" __|__________|__ Playlist ") 98 | print(" / o--▶︎--o \\ INDIE POP") 99 | print(" | ❄︎❄︎ | | ❄︎❄︎ |") 100 | print(" | ❄︎❄︎ | | ❄︎❄︎ | The place where you can vibe") 101 | print(" \\_______________/ Created By: Shellify • \(library.count) Songs, \(formatter.string(from:totalDuration)!)") 102 | print(" ") 103 | print(" ----------------------------------------------------------------------------------------------------------") 104 | print(" TITLE ARTIST ALBUM NAME") 105 | for song in library { 106 | if song.name.count < 36 { 107 | print(" \(song.name)", terminator:"") 108 | for _ in song.name.count ... 36 { 109 | print(" ", terminator:"") 110 | } 111 | } 112 | if song.artist.count < 36 { 113 | print("\(song.artist)", terminator:"") 114 | for _ in song.artist.count ... 36 { 115 | print(" ", terminator:"") 116 | } 117 | } 118 | print("\(song.albumName)") 119 | } 120 | print(" ----------------------------------------------------------------------------------------------------------") 121 | print("\n") 122 | print("Insert the name of which song you want to hear: ") 123 | } 124 | 125 | startProgram() 126 | 127 | let player: Player = Player.init() 128 | var userAnswer:String = "" 129 | 130 | userAnswer = readUserInput() 131 | 132 | do { 133 | try player.loadPlaylist(userPath : userAnswer) 134 | } catch { 135 | print("\nThere was a problem finding your song folder 😥\n") 136 | endProgram() 137 | exit(0) 138 | } 139 | 140 | print("\nLoading playlist...\n") 141 | showUserLibrary(library : player.songList) 142 | 143 | userAnswer = readUserInput() 144 | 145 | while userAnswer != "exit" { 146 | do { 147 | switch userAnswer { 148 | case "exit": 149 | break 150 | case "pause": 151 | if ((player.player?.isPlaying) != nil) { 152 | try player.pauseSong() 153 | } 154 | case "play": 155 | if ((player.player?.isPlaying) != nil) { 156 | try player.continueReproduction() 157 | } 158 | case "skip": 159 | if ((player.player?.isPlaying) != nil) { 160 | guard let nextSong: Song = try player.skipSong() else { 161 | throw ShellifyError.PlaybackError 162 | } 163 | playMusic(song: nextSong) 164 | } 165 | default: 166 | try player.playSong(songName: userAnswer) 167 | for song in player.songList { 168 | if song.name.localizedCaseInsensitiveContains(userAnswer) { 169 | playMusic(song : song) 170 | } 171 | } 172 | print("\nTo exit the program, type: exit") 173 | print("To pause a song, type: pause") 174 | print("To continue playing a song that was paused, type: play") 175 | print("To skip a song, type: skip") 176 | print("To listen another song, type its title\n") 177 | 178 | } 179 | 180 | } catch (ShellifyError.InvalidSongName) { 181 | print("Sorry, but it appers you've inserted a strange name for a song 😳") 182 | } catch (ShellifyError.PlaybackError) { 183 | print("Sorry, but it appears there is an error with the playback 😰") 184 | } catch (ShellifyError.SongFileNotFound) { 185 | print("Sorry, but it appears there was an error while loading the song file 😰") 186 | } catch (ShellifyError.SongNotFound) { 187 | print("Sorry, but you've tried to play a song that is not inside the folder 🤪") 188 | } catch (ShellifyError.SongParametrizationFailed) { 189 | print("Sorry, but there was an error adding your song 😰") 190 | } catch { 191 | print("Something went wrong 😰") 192 | } 193 | 194 | userAnswer = readUserInput() 195 | } 196 | 197 | endProgram() 198 | -------------------------------------------------------------------------------- /img-resources/catJAM.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarinaFX/Shellify/428c9c57077b9b5c389e9354383996b6e1fb3fbe/img-resources/catJAM.gif --------------------------------------------------------------------------------