Spotify
е платформа за stream
-ване на музика, която предоставя на потребителите достъп до милиони песни на изпълнители от цял свят.
Stream
-ването е метод за предаване на данни, използван обикновено за мултимедийни файлове. При него възпроизвеждането на съдържанието върху устройството на потребителя започва още с достъпването му, без да се налага то отначало да бъде изтеглено изцяло като файл и после да се стартира в подходящ плеър. Предаването на данните протича едновременно с възпроизвеждането, затова е необходима постоянна мрежова свързаност.
Създайте приложение по подобие на Spotify
, състоящо се от две части - сървър и клиент.
Java
version 19JUnit
version 5.8.1Mockito
version 5.1.1Google GSON
version 2.10.1
Предоставя следните функционалности на клиента:
- регистриране в платформата чрез email и парола (потребителите се съхраняват във файл)
- login в платформата чрез email и парола
- съхраняване на набор от песни, достъпни на потребителите за слушане (песните се съхраняват във файл)
- търсене на песни
- разглеждане на статистика на най-слушаните песни от потребителите
- създаване и изтриване на плейлисти (плейлистите се съхраняват във файлове)
- добавяне и изтриване на песни от плейлисти
- извличане на информация за даден плейлист
stream
-ване на песниstream
-ване на плейлисти (последователност от песни)
Spotify
клиентът представлява command line interface
със следните команди (в азбучен ред):
add-song-to <name_of_the_playlist> <title_of_the_song> : Add <song> to <playlist>
create-playlist <name_of_the_playlist> : Create <playlist>. The title of the playlist must be one-word and is case-sensitive
delete-playlist <name_of_the_playlist> : Delete <playlist>. The titles of the playlists are case-sensitive
disconnect : Disconnect from Spotify
help : List the current info
login <email> <password> : Log in Spotify
logout : Log out of Spotify
play <title_of_the_song> : Start playing the <song>
play-playlist <name_of_the_playlist> : Start playing all the songs of the <playlist> one after another. The title of the playlists are case-sensitive. If you want to stop or skip a song, you can stop it with the relevant command. In order to stop the playlist, you have to stop each song in it.
register <email> <password> : Registration in Spotify
remove-song-from <name_of_the_playlist> <title_of_the_song> : Remove <song> from <playlist>. The titles of the playlist are case-sensitive.
search <words> : Retrieve all songs whose title or artist contain all the words
show-playlist <name_of_the_playlist> : List the content of the <playlist>. The titles of the playlists are case-sensitive.
show-playlists : List the titles of all created playlists
stop : Stop playing the current song
top <number> : Retrieve list of the top <number> most listened songs
Структурата на кода по директории и пакети е следната:
src
└── bg.sofia.uni.fmi.mjt.spotify
├── client
│ ├── Client.java
├── server
│ ├── command
│ ├── Command.java
│ ├── CommandExecutor.java
│ ├── CommandExtractor.java
│ └── CommandName.java
│ ├── exceptions
│ ├── EmailAlreadyRegisteredException.java
│ ├── IODatabaseException.java
│ ├── LoggerNotCreatedSuccessfullyException.java
│ ├── NoSongPlayingException.java
│ ├── NoSongsInPlaylistException.java
│ ├── NoSuchPlaylistException.java
│ ├── NoSuchSongException.java
│ ├── NotValidEmailFormatException.java
│ ├── PlaylistAlreadyExistException.java
│ ├── PlaylistNotEmptyException.java
│ ├── SongAlreadyInPlaylistException.java
│ ├── SongIsAlreadyPlayingException.java
│ ├── SpotifyException.java
│ ├── UserAlreadyLoggedException.java
│ ├── UserNotFoundException.java
│ └── UserNotLoggedException.java
│ ├── logger
│ └── SpotifyLogger.java
│ ├── login
│ ├── AuthenticationService.java
│ ├── SHAAlgorithm.java
│ └── User.java
│ ├── player
│ ├── PlayPlaylistThread.java
│ └── PlaySongThread.java
│ ├── storage
│ ├── Playlist.java
│ ├── Song.java
│ └── SongEntity.java
├── Server.java
├── ServerReply.java
└── StreamingPlatform.java
test
└── bg.sofia.uni.fmi.mjt.spotify.server
├── command
│ ├── CommandExecutorTest.java
│ └── CommandExtractorTest.java
├── logger
│ └── SpotifyLoggerTest.java
├── login
│ ├── AuthenticationServiceTest.java
│ └── SHAAlgorithmTest.java
├── player
│ ├── PlayPlaylistThreadTest.java
│ └── PlaySongThreadTest.java
└── StreamingPlatformTest.java
data
├── authentication
│ └── RegisteredUsersList.txt
├── music
│ └── (...).wav
├── PlaylistList.json
├── SongsList.json
└── SpotifyLogger.log
Придържането към принципите за чист код беше водещо по време на разработването на проекта. Голяма част от литералите, както и всякакви "magic numbers" са изнесени като константи. Методите в класовете са с лимитирана дължина, както и изнесени в отделни private методи, където се налага. Всякакви отговори под формата на съобщения от сървъра са изнесени в enum клас. Част от правилата са проверени от инструмента за статичен анализ на кода, CheckStyle, с използвано, конкретно създаденo за MJT курса, множество от правила, които да напомнят за правилно форматиране и други правила за чист код.
Друг ключов момент от работата по проекта беше неговото тестване с JUnit и Mockito. Постигането на 84% code coverage беше предизвикателство, което несъмнено си заслужаваше, тъй като това до голяма степен верифицира правилното функциониране на кода. От друга страна, работата по създаването на тестове помогна за намирането и изчистването на бъгове в кода. Тестването на многонишков код изискваше използването на редица функции на Mockito, което беше поучително по отношение на усвояването на една нова, сравнително от скоро позната за мен, библиотека.
-
За да можете да се изпълняват песни от
Spotify
клиента е използвано API-тоjavax.sound.sampled
. -
javax.sound.sampled
работи само с файлове във wav формат, затова всички песни, които има на сървъра, трябва да са .wav -
javax.sound.sampled
предоставя два начина за възпроизвеждане на музика - чрезClip
иSourceDataLine
.Clip
се използва когато имамеnon-real-time
музикални данни (файл), които могат да бъдат предварително заредени в паметта.SourceDataLine
се използва заstream
-ване на данни, като например голям музикален файл, който не може да се зареди в паметта наведнъж, или за данни, които предварително не са известни. (за повече информация тук)За целите на проекта, е използван
SourceDataLine
.-
За да създадем
SourceDataLine
първо трябва да знаем конкретния формат на данните, които ще получаваме по мрежата. Това става с класаAudioFormat
. За да успеем да възпроизведем дадена песен при клиента, трябва предварително да знаем какъв е този формат. -
Преди сървърът да започне да ни
stream
-ва песента, той трябва да ни даде(прати) информация за формата на данните. КласътAudioFormat
не еSerializable
, т.е не можем да изпратим на клиента директно обект от типAudioFormat
. -
За да вземем формата на песента на сървъра, можем да използваме следния код:
AudioFormat audioFormat = AudioSystem.getAudioInputStream(new File(song)).getFormat();
-
Данните, които са необходими на клиента, са всички полета от конструктора на
AudioFormat
. Те могат да бъдат достъпени чрез съответнитеgetter
методи:AudioFormat(AudioFormat.Encoding encoding, float sampleRate, int sampleSizeInBits, int channels, int frameSize, float frameRate, boolean bigEndian)
-
След като сървърът е изпратил формата на данните, клиентът вече е готов да създаде съответния
SourceDataLine
обект, чрез който ще се възпроизвежда песента.Encoding encoding = ...; int sampleRate = ...; ... AudioFormat format = new AudioFormat(encoding, sampleRate, sampleSizeInBits, channels, frameSize, frameRate, bigEndian); DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); SourceDataLine dataLine = (SourceDataLine) AudioSystem.getLine(info); dataLine.open(); dataLine.start(); // Имайте предвид, че SourceDataLine.start() пуска нова нишка. За повече информация, може да проверите имплементацията.
-
За да запишем данни в
SourceDataLine
обекта (данните, които искаме да възпроизведем) използваме следния метод:dataLine.write(byte[] b, int off, int len);
-
За тестови цели, можем да си пуснем песен (non-real-time) със следния код:
AudioInputStream stream = AudioSystem.getAudioInputStream(new File("<music>.wav")); SourceDataLine dataLine = AudioSystem.getSourceDataLine(stream.getFormat()); dataLine.open(); dataLine.start(); while (true);
-
-
Валидиране на командите по подходящ начин.
-
Хврълените
exception
-и се записват във файл (logger).