Adding downloading functionality, left TODO's on where to pick up from

This commit is contained in:
kdeng00
2019-11-25 21:03:03 -05:00
parent d584c6c3af
commit d199b482ad
10 changed files with 278 additions and 275 deletions
+1 -2
View File
@@ -21,11 +21,9 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -O3 -std=c++17 -DNDEBUG")
set (SOURCES set (SOURCES
APIRepository.cpp APIRepository.cpp
BaseRepository.cpp BaseRepository.cpp
CoverArtRepository.cpp
Demo.cpp Demo.cpp
RepeatRepository.cpp RepeatRepository.cpp
ShuffleRepository.cpp ShuffleRepository.cpp
SongRepository.cpp
Tok.cpp Tok.cpp
TokenRepository.cpp TokenRepository.cpp
UserRepository.cpp UserRepository.cpp
@@ -49,6 +47,7 @@ set (HEADERS
Tok.h Tok.h
TokenRepository.h TokenRepository.h
UserRepository.h UserRepository.h
GeneralUtility.h
) )
set(JSON_BuildTests OFF CACHE INTERNAL "") set(JSON_BuildTests OFF CACHE INTERNAL "")
+65 -83
View File
@@ -21,94 +21,76 @@ namespace repository {
template<typename C> template<typename C>
class CoverArtRepository { class CoverArtRepository {
public: public:
std::vector<C> retrieveCoverArtRecords(const model::Token&, const std::string&); // TODO: implement this
std::vector<data> retrieveCoverArtData(const model::Token&, const C&, const std::string&); std::vector<C> retrieveCoverArtRecords(const model::Token& token, const std::string& uri) {
std::vector<std::string> vals;
return vals;
}
std::vector<data> retrieveCoverArtData(const model::Token& token, const C& cover,
const std::string& uri) {
std::string fullUri(uri);
if (fullUri.at(fullUri.size() - 1) != '/') {
fullUri.append("/");
}
fullUri.append(downloadEndpoint());
fullUri.append(std::to_string(cover.id));
CURL *curl;
struct curl_slist *chunk = nullptr;
curl = curl_easy_init();
if (!curl) {
return std::vector<data>();
}
std::string data;
std::string authHeader("Authorization: Bearer ");
authHeader.append(token.accessToken);
constexpr auto keepAliveHeader = "Content-type: Keep-alive";
chunk = curl_slist_append(chunk, authHeader.c_str());
chunk = curl_slist_append(chunk, keepAliveHeader);
curl_easy_setopt(curl, CURLOPT_URL, fullUri.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, respBodyRetriever);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
auto res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
std::vector<char> vals(data.begin(), data.end());
return vals;
}
// TODO: implement this
C retrieveCoverArtRecord(const model::Token& token, const C& cover,
const std::string& uri) {
C cov;
return cov;
}
C retrieveCoverArtRecord(const model::Token&, const C&, const std::string&);
private: private:
constexpr auto downloadEndpoint() noexcept; constexpr auto downloadEndpoint() noexcept {
constexpr auto recordsEndpoint() noexcept; return "api/v1/coverart/download/";
}
static size_t respBodyRetriever(void*, size_t, size_t, char*); constexpr auto recordsEndpoint() noexcept {
return "api/v1/coverart/";
}
static size_t respBodyRetriever(void *ptr, size_t size,
size_t nmemb, char *e) {
((std::string*)e)->append((char*)ptr, size * nmemb);
return size * nmemb;
}
}; };
template<typename C>
std::vector<C> CoverArtRepository<C>::retrieveCoverArtRecords(const model::Token& token,
const std::string& uri) {
std::vector<std::string> vals;
return vals;
}
template<typename C>
std::vector<data> CoverArtRepository<C>::retrieveCoverArtData(const model::Token& token,
const C& cover,
const std::string& uri) {
std::string fullUri(uri);
if (fullUri.at(fullUri.size() - 1) != '/') {
fullUri.append("/");
}
fullUri.append(downloadEndpoint());
fullUri.append(std::to_string(cover.id));
CURL *curl;
CURLcode res;
struct curl_slist *chunk = nullptr;
curl = curl_easy_init();
if (!curl) {
return std::vector<data>();
}
std::string data;
std::string authHeader("Authorization: Bearer ");
authHeader.append(token.accessToken);
constexpr auto keepAliveHeader = "Content-type: Keep-alive";
chunk = curl_slist_append(chunk, authHeader.c_str());
chunk = curl_slist_append(chunk, keepAliveHeader);
curl_easy_setopt(curl, CURLOPT_URL, fullUri.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, respBodyRetriever);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
std::stringstream buff(data);
std::vector<char> vals(data.begin(), data.end());
return vals;
}
template<typename C>
C CoverArtRepository<C>::retrieveCoverArtRecord(const model::Token& token,
const C& cover,
const std::string& uri) {
model::CoverArt cov;
return cov;
}
template<typename C>
constexpr auto CoverArtRepository<C>::downloadEndpoint() noexcept {
return "api/v1/coverart/download/";
}
template<typename C>
constexpr auto CoverArtRepository<C>::recordsEndpoint() noexcept {
return "api/v1/coverart/";
}
template<typename C>
size_t CoverArtRepository<C>::respBodyRetriever(void *ptr, size_t size, size_t nmemb, char *e) {
((std::string*)e)->append((char*)ptr, size * nmemb);
return size * nmemb;
}
} }
+18 -7
View File
@@ -388,6 +388,24 @@ Java_com_example_mear_repositories_TrackRepository_retrieveSong(
return fetchedSongObj; return fetchedSongObj;
} }
extern "C"
JNIEXPORT jobject
JNICALL
Java_com_example_mear_repositories_TrackRepositories_downloadSong(
JNIEnv *env,
jobject thisObj,
jobject tokenObj,
jobject songObj,
jstring uriStr
) {
// TODO: left off here
model::Song downloadedSong;
auto downloadedSongObj = songToObj(env, downloadedSong);
return downloadedSongObj;
}
extern "C" extern "C"
JNIEXPORT jobject JNIEXPORT jobject
JNICALL JNICALL
@@ -478,14 +496,7 @@ Java_com_example_mear_repositories_CoverArtRepository_retrieveCoverArtImage(
auto data = coverArtRepo.retrieveCoverArtData(token, cover, env->GetStringUTFChars(apiUri, nullptr)); auto data = coverArtRepo.retrieveCoverArtData(token, cover, env->GetStringUTFChars(apiUri, nullptr));
jbyteArray image = env->NewByteArray(data.size()); jbyteArray image = env->NewByteArray(data.size());
//for (auto& b: data) {
env->SetByteArrayRegion(image, 0, data.size(), (jbyte*) data.data()); env->SetByteArrayRegion(image, 0, data.size(), (jbyte*) data.data());
//}
//constexpr auto testImagePath = "/data/data/com.example.mear/image.png";
//std::ofstream out(testImagePath, std::ios::out | std::ios::binary);
//out.write(reinterpret_cast<char*>(data.data()), data.size());
//out.close();
return image; return image;
} }
+6 -96
View File
@@ -8,116 +8,26 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
namespace repository { namespace repository {
/**
std::vector<model::Song> SongRepository::fetchSongs(const model::Token& token, std::vector<model::Song> SongRepository::fetchSongs(const model::Token& token,
const std::string& uri) const std::string& uri)
{ {
std::string fullUri(uri);
if (fullUri.at(fullUri.size()-1) != '/') {
fullUri.append("/");
}
fullUri.append(songRecordEndpoint());
std::vector<model::Song> songs;
CURL *curl;
CURLcode res;
struct curl_slist *chunk = nullptr;
curl = curl_easy_init();
if (!curl) {
return songs;
}
std::string resp;
std::string authInfo("Authorization: Bearer ");
authInfo.append(token.accessToken);
constexpr auto contentType = "Content-type: Keep-alive";
chunk = curl_slist_append(chunk, authInfo.c_str());
chunk = curl_slist_append(chunk, contentType);
curl_easy_setopt(curl, CURLOPT_URL, fullUri.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, respBodyRetriever);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res == CURLE_OK) {
auto songsJson = nlohmann::json::parse(resp.c_str());
for (auto& songJson: songsJson) {
model::Song song(songJson["id"].get<int>(), songJson["title"].get<std::string>(),
songJson["artist"].get<std::string>(), songJson["album"].get<std::string>(),
songJson["genre"].get<std::string>(), songJson["duration"].get<int>(),
songJson["year"].get<int>());
song.albumArtist = songJson["album_artist"].get<std::string>();
song.coverArtId = songJson["coverart_id"].get<int>();
songs.push_back(song);
}
}
return songs;
} }
*/
/**
model::Song SongRepository::retrieveSong(const model::Token& token, const model::Song& sng, model::Song SongRepository::retrieveSong(const model::Token& token, const model::Song& sng,
const std::string& baseUri) { const std::string& baseUri) {
std::string uri(baseUri);
uri.append(songRecordEndpoint());
uri.append(std::to_string(sng.id));
model::Song song;
CURL *curl;
CURLcode res;
struct curl_slist *chunk = nullptr;
curl = curl_easy_init();
if (!curl) {
return sng;
}
auto resp = std::make_unique<char[]>(10000);
std::string authInfo("Authorization: Bearer ");
authInfo.append(token.accessToken);
constexpr auto contentType = "Content-type: Keep-alive";
chunk = curl_slist_append(chunk, authInfo.c_str());
chunk = curl_slist_append(chunk, contentType);
curl_easy_setopt(curl, CURLOPT_URL, uri.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, respBodyRetriever);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, resp.get());
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res == CURLE_OK) {
auto s = nlohmann::json::parse(resp.get());
song.id = s["id"].get<int>();
song.title = s["title"].get<std::string>();
song.album = s["album"].get<std::string>();
song.albumArtist = s["album_artist"].get<std::string>();
song.artist = s["artist"].get<std::string>();
song.genre = s["genre"].get<std::string>();
song.duration = s["duration"].get<int>();
song.year = s["year"].get<int>();
song.coverArtId = s["coverart_id"].get<int>();
return song;
}
return sng;
} }
*/
/**
size_t SongRepository::respBodyRetriever(void* ptr, size_t size, size_t nmemb, char *e) size_t SongRepository::respBodyRetriever(void* ptr, size_t size, size_t nmemb, char *e)
{ {
((std::string*)e)->append((char*)ptr, size * nmemb); ((std::string*)e)->append((char*)ptr, size * nmemb);
return size * nmemb; return size * nmemb;
} }
*/
} }
+153 -8
View File
@@ -8,25 +8,170 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include "model/Song.h" #include "model/Song.h"
#include "model/Token.h" #include "model/Token.h"
#include "GeneralUtility.h"
namespace repository { namespace repository {
template<class Song = model::Song>
class SongRepository { class SongRepository {
public: public:
std::vector<model::Song> fetchSongs(const model::Token&, const std::string&); template<typename Token = model::Token>
std::vector<Song> fetchSongs(const Token& token, const std::string& uri) {
auto fullUri = utility::GeneralUtility::appendForwardSlashToUri<std::string>(uri);
fullUri.append(songRecordEndpoint());
std::vector<Song> songs;
model::Song retrieveSong(const model::Token&, const model::Song&, const std::string&); CURL *curl;
CURLcode res;
struct curl_slist *chunk = nullptr;
curl = curl_easy_init();
if (!curl) {
return songs;
}
std::string resp;
std::string authInfo("Authorization: Bearer ");
authInfo.append(token.accessToken);
constexpr auto contentType = "Content-type: Keep-alive";
chunk = curl_slist_append(chunk, authInfo.c_str());
chunk = curl_slist_append(chunk, contentType);
curl_easy_setopt(curl, CURLOPT_URL, fullUri.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, respBodyRetriever);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res == CURLE_OK) {
auto songsJson = nlohmann::json::parse(resp.c_str());
for (auto& songJson: songsJson) {
Song song(songJson["id"].get<int>(), songJson["title"].get<std::string>(),
songJson["artist"].get<std::string>(),
songJson["album"].get<std::string>(),
songJson["album_artist"].get<std::string>(),
songJson["genre"].get<std::string>(), songJson["duration"].get<int>(),
songJson["year"].get<int>(), songJson["coverart_id"].get<int>());
/**
song.albumArtist = songJson["album_artist"].get<std::string>();
song.coverArtId = songJson["coverart_id"].get<int>();
*/
songs.push_back(song);
}
}
return songs;
}
template<typename Token = model::Token>
Song retrieveSong(const Token& token, const Song& sng,
const std::string& baseUri) {
auto uri = utility::GeneralUtility::appendForwardSlashToUri(baseUri);
uri.append(songRecordEndpoint());
uri.append(std::to_string(sng.id));
CURL *curl;
CURLcode res;
struct curl_slist *chunk = nullptr;
curl = curl_easy_init();
if (!curl) {
return sng;
}
std::string resp;
std::string authInfo("Authorization: Bearer ");
authInfo.append(token.accessToken);
constexpr auto contentType = "Content-type: Keep-alive";
chunk = curl_slist_append(chunk, authInfo.c_str());
chunk = curl_slist_append(chunk, contentType);
curl_easy_setopt(curl, CURLOPT_URL, uri.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, respBodyRetriever);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res == CURLE_OK) {
auto s = nlohmann::json::parse(resp.c_str());
Song song(s["id"].get<int>(), s["title"].get<std::string>(),
s["artist"].get<std::string>(), s["album"].get<std::string>(),
s["album_artist"].get<std::string>(), s["genre"].get<std::string>(),
s["duration"].get<int>(), s["year"].get<int>(),
s["coverart_id"].get<int>());
/**
song.id = s["id"].get<int>();
song.title = s["title"].get<std::string>();
song.album = s["album"].get<std::string>();
song.albumArtist = s["album_artist"].get<std::string>();
song.artist = s["artist"].get<std::string>();
song.genre = s["genre"].get<std::string>();
song.duration = s["duration"].get<int>();
song.year = s["year"].get<int>();
song.coverArtId = s["coverart_id"].get<int>();
*/
return song;
}
return sng;
}
template<typename Token = model::Token>
Song downloadSong(const Token &token, const Song song, const std::string& uri) {
auto fullUri = utility::GeneralUtility::appendForwardSlashToUri(uri);
fullUri.append(songDownloadEndpoint());
fullUri.append(std::to_string(song.id));
Song downloadedSong;
CURL *curl = curl_easy_init();
struct curl_slist *chunk = nullptr;
std::string data;
const auto authHeader = utility::GeneralUtility::authHeader(token);
chunk = curl_slist_append(chunk, authHeader.c_str());
chunk = curl_slist_append(chunk, "Content-type: Keep-alive");
curl_easy_setopt(curl, CURLOPT_URL, fullUri.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, respBodyRetriever);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
auto res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
downloadedSong.data = std::move(std::vector<char>(data.begin(), data.end()));
return downloadedSong;
}
private: private:
static size_t respBodyRetriever(void*, size_t, size_t, char*); static size_t respBodyRetriever(void *ptr, size_t size, size_t nmemb, char *e) {
((std::string*)e)->append((char*)ptr, size * nmemb);
return size * nmemb;
}
constexpr auto songRecordEndpoint() noexcept; constexpr auto songRecordEndpoint() noexcept {
return "api/v1/song/";
}
constexpr auto songDownloadEndpoint() noexcept {
return "api/v1/song/data/";
}
}; };
constexpr auto SongRepository::songRecordEndpoint() noexcept {
return "api/v1/song/";
}
} }
+16 -7
View File
@@ -6,6 +6,7 @@
#define MEAR_SONG_H #define MEAR_SONG_H
#include <string> #include <string>
#include <vector>
namespace model { namespace model {
@@ -20,22 +21,30 @@ public:
const int duration, const int year) : const int duration, const int year) :
id(id), title(title), artist(artist), album(album), id(id), title(title), artist(artist), album(album),
genre(genre), duration(duration), year(year) { } genre(genre), duration(duration), year(year) { }
Song(const int id, const std::string&& title, const std::string&& artist, Song(const int id, const std::string& title, const std::string& artist,
const std::string&& album, const std::string&& genre, const std::string& album, const std::string& albumArtist, const std::string& genre,
const int duration, const int year) : const int duration, const int year, const int coverArtId) :
id(id), title(std::move(title)), artist(std::move(artist)), id(id), title(title), artist(artist), album(album), albumArtist(albumArtist),
album(std::move(album)), genre(std::move(genre)), duration(duration), genre(genre), duration(duration), year(year), coverArtId(coverArtId) { }
year(year) { } /**
Song(const int id, const std::string&& title, const std::string&& artist,
const std::string&& album, const std::string&& genre,
const int duration, const int year) :
id(id), title(std::move(title)), artist(std::move(artist)),
album(std::move(album)), genre(std::move(genre)), duration(duration),
year(year) { }
*/
int id; int id;
std::string title; std::string title;
std::string artist; std::string artist;
std::string albumArtist;
std::string album; std::string album;
std::string albumArtist;
std::string genre; std::string genre;
int duration; int duration;
int year; int year;
int coverArtId; int coverArtId;
std::vector<char> data;
}; };
} }
@@ -368,6 +368,17 @@ class MainActivity : BaseServiceActivity() {
R.id.action_song_view -> { R.id.action_song_view -> {
startActivity(Intent(this, IcarusSongActivity::class.java)) startActivity(Intent(this, IcarusSongActivity::class.java))
} }
R.id.action_song_download -> {
// TODO: implement download functionality
val appPath = appDirectory()
val apiRepo = APIRepository()
val tokenRepo = TokenRepository()
val trackRepo = TrackRepository()
val song = musicService!!.getCurrentSong()
val token = tokenRepo.retrieveToken(appPath)
val apiInfo = apiRepo.retrieveRecord(appPath)
}
R.id.action_song_play_count-> { R.id.action_song_play_count-> {
val trk = musicService!!.getCurrentSong() val trk = musicService!!.getCurrentSong()
val pc = PlayCountRepository(this).getPlayCount(trk.id) val pc = PlayCountRepository(this).getPlayCount(trk.id)
@@ -21,6 +21,7 @@ class TrackRepository(var context: Context? = null) {
private external fun retrieveSongs(token: Token, uri: String): Array<Song> private external fun retrieveSongs(token: Token, uri: String): Array<Song>
private external fun retrieveSong(token: Token, song: Song, uri: String): Song private external fun retrieveSong(token: Token, song: Song, uri: String): Song
private external fun downloadSong(token: Token, song: Song, uri: String): Song
fun fetchSongs(token: Token, uri: String): Array<Song> { fun fetchSongs(token: Token, uri: String): Array<Song> {
@@ -32,55 +33,12 @@ class TrackRepository(var context: Context? = null) {
return retrieveSong(token, song, uri) return retrieveSong(token, song, uri)
} }
fun getAll(): List<Track> = context!!.database.use { fun fetchSongFile(token: Token, song: Song, uri: String): Song {
val tracks = mutableListOf<Track>() val downloadedSong = downloadSong(token, song, uri)
select("Track" ) return downloadedSong
.parseList(object : MapRowParser<Track>{
override fun parseRow(columns: Map<String, Any?>): Track {
val id = columns.getValue("Id")
val title = columns.getValue("Title")
val artist = columns.getValue("Artist").toString()
val album = columns.getValue("Album").toString()
val duration = columns.getValue("Duration").toString().toInt()
val songPath = columns.getValue("FilePath").toString()
val track = Track(id.toString().toInt(), title.toString(),artist,album,
duration, ByteArray(0),songPath)
tracks.add(track)
return track
}
})
tracks
} }
fun getTrack(id: Int): Track = context!!.database.use {
select("Track").where("Id = $id")
.parseSingle(object: MapRowParser<Track>{
override fun parseRow(columns: Map<String, Any?>): Track {
val id = columns.getValue("Id").toString().toInt()
val title = columns.getValue("Title").toString()
val artist = columns.getValue("Artist").toString()
var album = columns.getValue("Album").toString()
val duration = columns.getValue("Duration").toString().toInt()
var filePath = columns.getValue("FilePath").toString()
val track = Track(id, title, artist, album, duration,
ByteArray(0), filePath)
return track
}
})
}
fun getSongCount(): Int = context!!.database.use {
select("TrackCount").limit(1)
.parseSingle(object : MapRowParser<Int>{
override fun parseRow(columns: Map<String, Any?>): Int {
return columns.getValue("TotalSongs").toString().toInt()
}
})
}
fun insertTracks(tracks: List<Track>) = context!!.database.use { fun insertTracks(tracks: List<Track>) = context!!.database.use {
transaction { transaction {
@@ -111,30 +69,4 @@ class TrackRepository(var context: Context? = null) {
fun delete() = context!!.database.use { fun delete() = context!!.database.use {
delete("Track") delete("Track")
} }
fun deleteTracks(tracks: List<Track>) = context!!.database.use {
tracks.iterator().forEach {
transaction {
delete("Track", "id = {track_id}", "track_id" to it.id)
}
}
}
fun deleteTracksByPaths(trackPaths: List<String>) = context!!.database.use {
trackPaths.iterator().forEach {
transaction {
delete("Track", "FilePath = {file_path}", "file_path" to it)
}
}
}
fun getLibraryCount(): Int? {
context!!.database.use {
select("TrackCount").limit(1)
.parseList (object : MapRowParser<Int> {
override fun parseRow(columns: Map<String, Any?>): Int {
return columns.getValue("TotalSongs").toString().toInt()
}
})
}
return 0
}
} }
+3
View File
@@ -7,6 +7,9 @@
<item android:id="@+id/action_song_view" <item android:id="@+id/action_song_view"
android:title="@string/song_view" android:title="@string/song_view"
app:showAsAction="never" /> app:showAsAction="never" />
<item android:id="@+id/action_song_download"
android:title="@string/song_download"
app:showAsAction="never" />
<item android:id="@+id/action_song_play_count" <item android:id="@+id/action_song_play_count"
android:title="@string/song_play_count" android:title="@string/song_play_count"
app:showAsAction="never" /> app:showAsAction="never" />
+1
View File
@@ -37,6 +37,7 @@
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="song_view">Songs</string> <string name="song_view">Songs</string>
<string name="song_play_count">Play Count</string> <string name="song_play_count">Play Count</string>
<string name="song_download">Download</string>
<!-- Popups --> <!-- Popups -->
<string name="popup_about_header">About</string> <string name="popup_about_header">About</string>