ADD: Load Remote Songs from Server

This commit is contained in:
Fawkes100 2025-01-19 15:53:11 +01:00
parent a53bc4bee7
commit d059f7ed8e
9 changed files with 302 additions and 5 deletions

View File

@ -69,4 +69,13 @@ public interface SongDao {
@Query("SELECT * FROM NoteSheet WHERE songID IN (:localSongIDs)")
List<NoteSheet> getNoteSheetFilesBySongIDs(List<Integer> localSongIDs);
@Insert
void insertSongs(List<Song> onlyRemoteExistingSongs);
@Delete
void deleteSong(Song song);
@Query("DELETE FROM NoteSheet WHERE songID = :localID")
void deleteNoteSheetsByLocalSongID(int localID);
}

View File

@ -4,6 +4,7 @@ import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.stormtales.notevault.data.sync.SyncStatus;
import com.stormtales.notevault.network.sync.models.SongModel;
import java.time.LocalDateTime;
import java.util.Objects;
@ -32,6 +33,17 @@ public class Song {
this.syncStatus = SyncStatus.CREATED;
}
@Ignore
public Song(SongModel songModel) {
this.serverID = songModel.getServerID();
this.title = songModel.getTitle();
this.composer = songModel.getComposer();
this.genre = songModel.getGenre();
this.year = songModel.getYear();
this.syncStatus = SyncStatus.SYNCED;
this.syncTime = LocalDateTime.now();
}
public Song() {
}

View File

@ -10,8 +10,10 @@ import com.stormtales.notevault.data.entities.NoteSheet;
import com.stormtales.notevault.data.entities.Song;
import com.stormtales.notevault.data.sync.SyncStatus;
import com.stormtales.notevault.network.sync.models.*;
import com.stormtales.notevault.utils.NoteSheetsUtil;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
@ -173,6 +175,81 @@ public class SongSyncRepository {
});
}
public void loadRemotelyModifiedSongs(LoadDataCallback<List<Song>> callback) {
Executors.newSingleThreadExecutor().execute(() -> {
List<Song> songs = songDao.getSongsBySyncStatus(SyncStatus.REMOTE_MODIFIED);
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(()-> callback.onResult(songs));
});
}
public void saveRemoteSongs(List<SongModel> remoteSongs, LoadDataCallback<List<NoteSheet>> callback) {
Executors.newSingleThreadExecutor().execute(() ->{
List<String> serverIDs = remoteSongs.stream().map(SongModel::getServerID).collect(Collectors.toList());
List<Song> songs = songDao.getSongsByServerIDs(serverIDs);
List<Integer> localSongIDs = songDao.getLocalSongIDsByServerIDs(serverIDs);
List<NoteSheet> noteSheets = songDao.getNoteSheetsBySongIDs(localSongIDs);
List<Song> onlyRemoteExistingSongs = new ArrayList<>();
List<NoteSheet> outdatedNoteSheets = new ArrayList<>();
for(SongModel songModel : remoteSongs) {
boolean found = false;
for(Song song : songs) {
if(song.getServerID().equals(songModel.getServerID())) {
found = true;
if(!songModel.isDeleted()) {
song.setSyncStatus(SyncStatus.SYNCED);
song.setSyncTime(LocalDateTime.now());
song.setTitle(songModel.getTitle());
song.setComposer(songModel.getComposer());
song.setGenre(songModel.getGenre());
song.setYear(songModel.getYear());
for(NoteSheet noteSheet : noteSheets) {
for(RemotelyModifiedNoteSheetModel remoteNoteSheet : songModel.getNote_sheets()) {
if(remoteNoteSheet.getServer_filename().equals(noteSheet.getServerFileName())) {
if(!remoteNoteSheet.getHash().equals(noteSheet.getHash())) {
outdatedNoteSheets.add(noteSheet);
break;
}
}
}
}
} else {
removeSong(song);
}
}
}
if(!found) {
Song song = new Song(songModel);
onlyRemoteExistingSongs.add(song);
}
}
songDao.updateSongs(songs);
songDao.insertSongs(onlyRemoteExistingSongs);
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(()-> callback.onResult(outdatedNoteSheets));
});
}
private void removeSong(Song song) {
List<NoteSheet> noteSheets = songDao.getNoteSheetsBySong(song.getLocalID());
for(NoteSheet noteSheet : noteSheets) {
try {
NoteSheetsUtil.deleteNoteSheet(noteSheet.getLocalFileName());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
songDao.deleteNoteSheets(noteSheets);
songDao.deleteSong(song);
}
public interface LoadDataCallback<T> {
void onResult(T result);
}

View File

@ -5,6 +5,7 @@ import com.stormtales.notevault.network.auth.LoginResponse;
import com.stormtales.notevault.network.sync.models.*;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.*;
@ -31,4 +32,10 @@ public interface SongSyncAPI {
@GET("/sync/songs/fetch")
Call<FetchResponse> fetchRemoteModifiedSongs(@Query(value = "last_client_sync") String last_client_sync);
@GET("/sync/songs/get")
Call<SongModel> fetchRemotelyModifiedSongData(@Query(value = "songID") String songID);
@GET("/sync/songs/get/notesheet/")
Call<ResponseBody> downloadNotesheet(@Query("server_filename") String server_filename);
}

View File

@ -1,12 +1,16 @@
package com.stormtales.notevault.network.sync;
import android.content.Context;
import android.net.Uri;
import com.stormtales.notevault.data.entities.NoteSheet;
import com.stormtales.notevault.data.entities.Song;
import com.stormtales.notevault.data.repositories.SongRepository;
import com.stormtales.notevault.data.repositories.SongSyncRepository;
import com.stormtales.notevault.network.sync.models.*;
import com.stormtales.notevault.ui.gallery.GalleryViewModel;
import com.stormtales.notevault.utils.NoteSheetsUtil;
import java.io.File;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@ -17,12 +21,14 @@ public class SongSyncModule {
private SongSyncRepository songSyncRepository;
private SongSyncService songSyncService;
private GalleryViewModel syncViewModel;
private Context context;
public SongSyncModule(Context context, GalleryViewModel syncViewModel) {
this.songRepository = new SongRepository(context);
this.songSyncRepository = new SongSyncRepository(context);
this.songSyncService = new SongSyncService(context);
this.syncViewModel = syncViewModel;
this.context = context;
}
public void syncCreatedSongs() {
@ -84,11 +90,32 @@ public class SongSyncModule {
@Override
public void onResult(FetchResponse result) {
songSyncRepository.markSongsAsRemotelyModified(result.getServerIDs());
syncViewModel.finishFetching();
getRemotelyModifiedSongData();
}
});
}
public void getRemotelyModifiedSongData() {
songSyncRepository.loadRemotelyModifiedSongs(songs -> {
songSyncService.getRemotelyModifiedSongData(songs, remoteSongs -> {
songSyncRepository.saveRemoteSongs(remoteSongs, this::downloadNoteSheets);
});
});
}
public void downloadNoteSheets(List<NoteSheet> noteSheets) {
for(NoteSheet noteSheet : noteSheets) {
songSyncService.downloadNoteSheet(noteSheet, responseBody -> {
String localFilename = noteSheet.getServerFileName().substring(36);
NoteSheetsUtil.saveImageFromServer(responseBody, context.getFilesDir());
});
}
syncViewModel.finishFetching();
}
public interface FinishSongCreateSyncingCallback {
void finishSongSyncing(BatchCreateResponse response);
}

View File

@ -12,6 +12,7 @@ import com.stormtales.notevault.ui.gallery.GalleryViewModel;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -176,6 +177,44 @@ public class SongSyncService {
});
}
public void getRemotelyModifiedSongData(List<Song> remotelyModifiedSongs, SongSyncRepository.LoadDataCallback<List<SongModel>> callback) {
List<SongModel> songModels = new ArrayList<>();
for(Song song : remotelyModifiedSongs) {
songSyncAPI.fetchRemotelyModifiedSongData(song.getServerID()).enqueue(new Callback<SongModel>() {
@Override
public void onResponse(Call<SongModel> call, Response<SongModel> response) {
if(response.isSuccessful() && response.body() != null) {
songModels.add(response.body());
if(songModels.size() == remotelyModifiedSongs.size()) {
callback.onResult(songModels);
}
}
}
@Override
public void onFailure(Call<SongModel> call, Throwable throwable) {
Log.d("SongSyncService", "Fetch failed: " + throwable.getMessage(), throwable);
}
});
}
}
public void downloadNoteSheet(NoteSheet noteSheet, SongSyncRepository.LoadDataCallback<ResponseBody> callback) {
songSyncAPI.downloadNotesheet(noteSheet.getServerFileName()).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if(response.isSuccessful() && response.body() != null) {
callback.onResult(response.body());
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable throwable) {
Log.d("SongSyncService", "Download failed: " + throwable.getMessage(), throwable);
}
});
}
public interface SyncDeletedSongsCallback {
void finishSongSyncing(List<String> remoteDeletedSongs, List<Integer> localDeletedSongs);

View File

@ -0,0 +1,31 @@
package com.stormtales.notevault.network.sync.models;
public class RemotelyModifiedNoteSheetModel {
private String filename;
private String server_filename;
private String hash;
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getServer_filename() {
return server_filename;
}
public void setServer_filename(String server_filename) {
this.server_filename = server_filename;
}
public String getHash() {
return hash;
}
public void setHash(String hash) {
this.hash = hash;
}
}

View File

@ -0,0 +1,69 @@
package com.stormtales.notevault.network.sync.models;
import java.util.List;
public class SongModel {
private String serverID;
private String title;
private String composer;
private String genre;
private int year;
private List<RemotelyModifiedNoteSheetModel> note_sheets;
private boolean deleted;
public String getServerID() {
return serverID;
}
public void setServerID(String serverID) {
this.serverID = serverID;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getComposer() {
return composer;
}
public void setComposer(String composer) {
this.composer = composer;
}
public String getGenre() {
return genre;
}
public void setGenre(String genre) {
this.genre = genre;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public List<RemotelyModifiedNoteSheetModel> getNote_sheets() {
return note_sheets;
}
public void setNote_sheets(List<RemotelyModifiedNoteSheetModel> note_sheets) {
this.note_sheets = note_sheets;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
}

View File

@ -4,11 +4,9 @@ import android.content.ContentResolver;
import android.content.Context;
import android.media.ExifInterface;
import android.net.Uri;
import okhttp3.ResponseBody;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.*;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -85,6 +83,34 @@ public class NoteSheetsUtil {
}
}
public static Tupel<String, String> saveImageFromServer(ResponseBody body, File filesDir) {
File imageFile = new File(filesDir, "saved_image_" + System.currentTimeMillis() + ".jpg");
try (InputStream inputStream = body.byteStream();
FileOutputStream outputStream = new FileOutputStream(imageFile)) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// Daten blockweise lesen und gleichzeitig schreiben und hashen
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
digest.update(buffer, 0, bytesRead); // Hash aktualisieren
}
// Hash berechnen
byte[] hashBytes = digest.digest();
StringBuilder hashString = new StringBuilder();
for (byte b : hashBytes) {
hashString.append(String.format("%02x", b)); // Bytes in Hexadezimal umwandeln
}
// Hashwert zurückgeben (kann auch zusammen mit dem Pfad gespeichert werden)
return new Tupel<>(imageFile.getAbsolutePath(), hashString.toString());
} catch (IOException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static void deleteNoteSheet(String filePath) throws IOException {
Files.delete(new File(filePath).toPath());
}