diff --git a/README.md b/README.md index 5150e50f..05def9a9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/_uV8Mn8f) # 📘 Projektarbete: JPA + Hibernate med GitHub-flöde Projektet genomförs som antingen en Java CLI-applikation eller med hjĂ€lp av JavaFX om ni vill ha ett grafiskt grĂ€nssnitt. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..fcd736ac --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + myPod: + image: mysql:9.5.0 + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_ROOT_HOST: "%" + MYSQL_DATABASE: myPodDB + MYSQL_USER: user + MYSQL_PASSWORD: pass + ports: + - "3306:3306" + volumes: + - mypod_data:/var/lib/mysql +volumes: + mypod_data: diff --git a/pom.xml b/pom.xml index 909503d0..d68424a5 100644 --- a/pom.xml +++ b/pom.xml @@ -45,10 +45,64 @@ 9.5.0 runtime + + com.fasterxml.jackson.core + jackson-databind + 2.17.1 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.17.1 + io.github.classgraph classgraph 4.8.184 + + org.openjfx + javafx-controls + 25 + + + org.openjfx + javafx-fxml + 25 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 25 + 25 + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + + + default-cli + + org.example.MyPod + app + app + app + true + true + true + + + + + + diff --git a/src/main/java/org/example/App.java b/src/main/java/org/example/App.java index 165e5cd5..b9d5de88 100644 --- a/src/main/java/org/example/App.java +++ b/src/main/java/org/example/App.java @@ -1,7 +1,12 @@ package org.example; +import javafx.application.Application; + + public class App { public static void main(String[] args) { - System.out.println("Hello There!"); + + Application.launch(MyPod.class, args); + } } diff --git a/src/main/java/org/example/DatabaseInitializer.java b/src/main/java/org/example/DatabaseInitializer.java new file mode 100644 index 00000000..92eda0f4 --- /dev/null +++ b/src/main/java/org/example/DatabaseInitializer.java @@ -0,0 +1,67 @@ +package org.example; + +import org.example.entity.Album; +import org.example.entity.Artist; +import org.example.entity.Song; +import org.example.repo.AlbumRepository; +import org.example.repo.ArtistRepository; +import org.example.repo.SongRepository; + +import java.util.List; + +public class DatabaseInitializer { + + private final ItunesApiClient apiClient; + + private final SongRepository songRepo; + private final AlbumRepository albumRepo; + private final ArtistRepository artistRepo; + + public DatabaseInitializer(ItunesApiClient apiClient, SongRepository songRepo , AlbumRepository albumRepo, ArtistRepository artistRepo) { + this.apiClient = apiClient; + this.songRepo = songRepo; + this.albumRepo = albumRepo; + this.artistRepo = artistRepo; + } + + public void init() { + if (songRepo.count() > 0) { //check if there is data already + return; + } + + List searches = List.of("the+war+on+drugs", + "refused", + "thrice", + "16+horsepower", + "viagra+boys", + "geese", + "ghost", + "run+the+jewels", + "rammstein", + "salvatore+ganacci", + "baroness" + ); + for (String term : searches) { + try { + apiClient.searchSongs(term).forEach(dto -> { + Artist ar = Artist.fromDTO(dto); + if (!artistRepo.existsByUniqueId(ar)) { + artistRepo.save(ar); + } + + Album al = Album.fromDTO(dto, ar); + if (!albumRepo.existsByUniqueId(al)) { + albumRepo.save(al); + } + + Song s = Song.fromDTO(dto, al); + if (!songRepo.existsByUniqueId(s)) { + songRepo.save(s); + } + }); + } catch (Exception e) { + throw new RuntimeException("Failed to fetch or persist data for search term: " + term, e); + } + } + } +} diff --git a/src/main/java/org/example/ItunesApiClient.java b/src/main/java/org/example/ItunesApiClient.java new file mode 100644 index 00000000..b0184f0f --- /dev/null +++ b/src/main/java/org/example/ItunesApiClient.java @@ -0,0 +1,63 @@ +package org.example; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public class ItunesApiClient { + + private final HttpClient client; + private final ObjectMapper mapper; + + public ItunesApiClient() { + this.client = HttpClient.newHttpClient(); + this.mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + } + + public List searchSongs(String term) throws Exception { + + String encodedTerm = URLEncoder.encode(term, StandardCharsets.UTF_8); + String url = "https://itunes.apple.com/search?term=" + encodedTerm + "&entity=song&attribute=artistTerm&limit=8"; + + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(10)) + .build(); + + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString()); + + // Kontrollera status + if (response.statusCode() != 200) { + throw new RuntimeException("API-fel: " + response.statusCode()); + } + + // Parse JSON + JsonNode root = mapper.readTree(response.body()); + JsonNode results = root.get("results"); + if (results == null || !results.isArray()) { + return List.of(); + } + + List songs = new ArrayList<>(); + for (JsonNode node : results) { + ItunesDTO song = mapper.treeToValue(node, ItunesDTO.class); + songs.add(song); + } + + return songs; + } +} + diff --git a/src/main/java/org/example/ItunesDTO.java b/src/main/java/org/example/ItunesDTO.java new file mode 100644 index 00000000..c85e89ac --- /dev/null +++ b/src/main/java/org/example/ItunesDTO.java @@ -0,0 +1,23 @@ +package org.example; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.time.LocalDate; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record ItunesDTO(Long artistId, + Long collectionId, + Long trackId, + String trackName, + String artistName, + String collectionName, + String country, + String primaryGenreName, + LocalDate releaseDate, + Long trackCount, + Long trackTimeMillis) { + + public int releaseYear() { + return releaseDate != null ? releaseDate.getYear() : 0; + } +} diff --git a/src/main/java/org/example/ItunesPlayList.java b/src/main/java/org/example/ItunesPlayList.java new file mode 100644 index 00000000..d637f428 --- /dev/null +++ b/src/main/java/org/example/ItunesPlayList.java @@ -0,0 +1,345 @@ +package org.example; + +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Text; +import javafx.stage.Stage; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Huvudklass för GUI:t. Hanterar visning av bibliotek, spellistor och sökning. + */ +public class ItunesPlayList { + + // --- DATAMODELLER --- + + // En Map som lagrar alla spellistor. Nyckeln Ă€r namnet (t.ex. "Musik") och vĂ€rdet Ă€r listan med lĂ„tar. + private Map> allPlaylists = new HashMap<>(); + + // Listan med namn pĂ„ spellistor som visas i vĂ€nstermenyn (Sidebar). + // "ObservableList" gör att GUI:t uppdateras automatiskt om vi lĂ€gger till/tar bort namn hĂ€r. + private ObservableList playlistNames = FXCollections.observableArrayList(); + + // --- GUI KOMPONENTER --- + + // Tabellen i mitten som visar lĂ„tarna + private TableView songTable = new TableView<>(); + + // Listan till vĂ€nster dĂ€r man vĂ€ljer spellista + private ListView sourceList = new ListView<>(); + + // TextfĂ€lt för den "digitala displayen" högst upp + private Text lcdTitle = new Text("myTunes"); + private Text lcdArtist = new Text("VĂ€lj bibliotek eller spellista"); + + /** + * Bygger upp hela grĂ€nssnittet och visar fönstret. + * @param dbSongs En lista med lĂ„tar hĂ€mtade frĂ„n databasen/backend. + */ + public void showLibrary(List dbSongs) { + Stage stage = new Stage(); + + // Konvertera databas-objekten till vĂ„r interna DisplaySong-klass och skapa grundlistorna + initData(dbSongs); + + // BorderPane Ă€r huvudlayouten: Top, Left, Center, Bottom + BorderPane root = new BorderPane(); + + // --------------------------------------------------------- + // 1. TOPPEN (Knappar, LCD-display, SökfĂ€lt) + // --------------------------------------------------------- + HBox topPanel = new HBox(15); // HBox lĂ€gger saker pĂ„ rad horisontellt + topPanel.getStyleClass().add("top-panel"); // CSS-klass för styling + topPanel.setPadding(new Insets(10, 15, 10, 15)); + topPanel.setAlignment(Pos.CENTER_LEFT); + + // Skapa LCD-displayen (den blĂ„ rutan med text) + StackPane lcdDisplay = createLCDDisplay(); + // SĂ€g Ă„t displayen att vĂ€xa och ta upp ledig plats i bredd + HBox.setHgrow(lcdDisplay, Priority.ALWAYS); + + // SökfĂ€ltet + TextField searchField = new TextField(); + searchField.setPromptText("Sök..."); + searchField.getStyleClass().add("itunes-search"); + + // Lyssnare: NĂ€r texten Ă€ndras i sökfĂ€ltet, kör metoden filterSongs() + searchField.textProperty().addListener((obs, old, newVal) -> filterSongs(newVal)); + + // LĂ€gg till allt i toppen + topPanel.getChildren().addAll( + createRoundButton("⏼"), createRoundButton("▶"), createRoundButton("⏭"), + lcdDisplay, searchField + ); + + // --------------------------------------------------------- + // 2. VÄNSTER (Spellistorna) + // --------------------------------------------------------- + sourceList.setItems(playlistNames); // Koppla data till listan + sourceList.getStyleClass().add("source-list"); + sourceList.setPrefWidth(200); + + // Lyssnare: Vad hĂ€nder nĂ€r man klickar pĂ„ en spellista i menyn? + sourceList.getSelectionModel().selectedItemProperty().addListener((obs, old, newVal) -> { + if (newVal != null) { + searchField.clear(); // Rensa gammal sökning + // HĂ€mta lĂ„tlistan frĂ„n vĂ„r Map baserat pĂ„ namnet och visa i tabellen + songTable.setItems(allPlaylists.get(newVal)); + } + }); + sourceList.getSelectionModel().selectFirst(); // VĂ€lj första listan ("Musik") som startvĂ€rde + + // --------------------------------------------------------- + // 3. MITTEN (LĂ„ttabellen) + // --------------------------------------------------------- + setupTable(); // Konfigurerar kolumner och beteende för tabellen + + // --------------------------------------------------------- + // 4. BOTTEN (Knappar för att hantera listor) + // --------------------------------------------------------- + HBox bottomPanel = new HBox(10); + bottomPanel.setPadding(new Insets(10)); + bottomPanel.getStyleClass().add("bottom-panel"); + + Button btnAddList = new Button("+"); + btnAddList.getStyleClass().add("list-control-button"); + Button btnDeleteList = new Button("-"); + btnDeleteList.getStyleClass().add("list-control-button"); + Button btnMoveToPlaylist = new Button("LĂ€gg till LĂ„t i spellista"); + Button btnRemoveSong = new Button("Ta bort lĂ„t frĂ„n lista"); + + // Koppla knapparna till metoder + btnAddList.setOnAction(e -> createNewPlaylist()); + btnDeleteList.setOnAction(e -> deleteSelectedPlaylist()); + btnRemoveSong.setOnAction(e -> removeSelectedSong()); + btnMoveToPlaylist.setOnAction(e -> addSelectedSong(btnMoveToPlaylist)); + + bottomPanel.getChildren().addAll(btnAddList, btnDeleteList, new Separator(), btnMoveToPlaylist, btnRemoveSong); + + // --------------------------------------------------------- + // SLUTMONTERING + // --------------------------------------------------------- + + // SplitPane gör att anvĂ€ndaren kan dra i grĂ€nsen mellan vĂ€nstermeny och tabell + SplitPane splitPane = new SplitPane(sourceList, songTable); + splitPane.setDividerPositions(0.25); // SĂ€tt startposition för avdelaren + + root.setTop(topPanel); + root.setCenter(splitPane); + root.setBottom(bottomPanel); + + Scene scene = new Scene(root, 950, 600); + // Ladda CSS-filen (mĂ„ste ligga i resources-mappen) + scene.getStylesheets().add(getClass().getResource("/ipod_style.css").toExternalForm()); + + stage.setScene(scene); + stage.setTitle("myTunes"); + stage.show(); + } + + /** + * HjĂ€lpmetod för att skapa LCD-displayen (bakgrund + text). + */ + private StackPane createLCDDisplay() { + StackPane stack = new StackPane(); + Rectangle bg = new Rectangle(350, 45); + bg.getStyleClass().add("lcd-background"); + + VBox textStack = new VBox(2); + textStack.setAlignment(Pos.CENTER); + lcdTitle.getStyleClass().add("lcd-title"); + lcdArtist.getStyleClass().add("lcd-artist"); + + textStack.getChildren().addAll(lcdTitle, lcdArtist); + stack.getChildren().addAll(bg, textStack); + return stack; + } + + /** + * HjĂ€lpmetod för att skapa en standardiserad knapp. + */ + private Button createRoundButton(String icon) { + Button b = new Button(icon); + b.getStyleClass().add("itunes-button"); + return b; + } + + /** + * Konfigurerar kolumnerna i tabellen och hur data ska visas. + */ + private void setupTable() { + // Skapa kolumner + TableColumn titleCol = new TableColumn<>("Namn"); + // BerĂ€tta för kolumnen vilket fĂ€lt i DisplaySong den ska lĂ€sa frĂ„n (name) + titleCol.setCellValueFactory(d -> new SimpleStringProperty(d.getValue().name)); + + TableColumn artistCol = new TableColumn<>("Artist"); + artistCol.setCellValueFactory(d -> new SimpleStringProperty(d.getValue().artist)); + + TableColumn albumCol = new TableColumn<>("Album"); + albumCol.setCellValueFactory(d -> new SimpleStringProperty(d.getValue().album)); + + TableColumn timeCol = new TableColumn<>("LĂ€ngd"); + timeCol.setCellValueFactory(d -> new SimpleStringProperty(d.getValue().time)); + + songTable.getColumns().setAll(titleCol, artistCol, albumCol, timeCol); + songTable.getStyleClass().add("song-table"); + + // Gör sĂ„ att kolumnerna fyller ut hela bredden + songTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + // Lyssnare: NĂ€r man klickar pĂ„ en rad i tabellen -> Uppdatera LCD-displayen + songTable.getSelectionModel().selectedItemProperty().addListener((obs, old, newVal) -> { + if (newVal != null) { + lcdTitle.setText(newVal.name); + lcdArtist.setText(newVal.artist); + } + }); + } + + /** + * Filtrerar lĂ„tarna i den aktiva listan baserat pĂ„ söktexten. + */ + private void filterSongs(String searchText) { + String currentList = sourceList.getSelectionModel().getSelectedItem(); + if (currentList == null) return; + + // HĂ€mta originaldatan för den valda spellistan + ObservableList masterData = allPlaylists.get(currentList); + + // Om sökfĂ€ltet Ă€r tomt, visa allt + if (searchText == null || searchText.isEmpty()) { + songTable.setItems(masterData); + return; + } + + // Skapa en filtrerad lista som omsluter masterData + FilteredList filteredData = new FilteredList<>(masterData, song -> { + String filter = searchText.toLowerCase(); + // Returnera true om sökordet finns i namn, artist eller album + return song.name.toLowerCase().contains(filter) || + song.artist.toLowerCase().contains(filter) || + song.album.toLowerCase().contains(filter); + }); + + songTable.setItems(filteredData); + } + + /** + * Omvandlar databas-objekten till GUI-objekt och skapar standardlistor. + */ + private void initData(List dbSongs) { + ObservableList library = FXCollections.observableArrayList(); + + // Loopa igenom datan frĂ„n databasen + if (dbSongs != null) { + for (org.example.entity.Song s : dbSongs) { + // Hantera null-vĂ€rden snyggt (om artist eller album saknas) + String art = (s.getAlbum() != null && s.getAlbum().getArtist() != null) ? s.getAlbum().getArtist().getName() : "OkĂ€nd"; + String alb = (s.getAlbum() != null) ? s.getAlbum().getName() : "OkĂ€nt"; + + // Skapa ett nytt DisplaySong-objekt + library.add(new DisplaySong(s.getTitle(), art, alb, s.getLength())); + } + } + + // LĂ€gg in huvudbiblioteket "Musik" + allPlaylists.put("Musik", library); + playlistNames.add("Musik"); + + // Skapa en tom lista för "Favoriter" + allPlaylists.put("Favoriter", FXCollections.observableArrayList()); + playlistNames.add("Favoriter"); + } + + /** + * Visar en dialogruta för att skapa en ny spellista. + */ + private void createNewPlaylist() { + TextInputDialog d = new TextInputDialog("Ny lista"); + + // HĂ€r Ă€ndrar du fönstrets titel och text + d.setTitle("Skapa ny spellista"); // ErsĂ€tter "BekrĂ€ftelse" + d.setHeaderText("Ange namn pĂ„ din nya lista"); // Rubriken inuti rutan + d.setContentText("Namn:"); // Texten bredvid inmatningsfĂ€ltet + + d.showAndWait().ifPresent(name -> { + // Kontrollera att namnet inte Ă€r tomt och inte redan finns + if (!name.trim().isEmpty() && !allPlaylists.containsKey(name)) { + allPlaylists.put(name, FXCollections.observableArrayList()); + playlistNames.add(name); + } + }); + } + + /** + * Tar bort vald spellista (men tillĂ„ter inte att man tar bort "Musik"). + */ + private void deleteSelectedPlaylist() { + String sel = sourceList.getSelectionModel().getSelectedItem(); + if (sel != null && !sel.equals("Musik")) { + allPlaylists.remove(sel); + playlistNames.remove(sel); + } + } + + /** + * Tar bort vald lĂ„t frĂ„n den aktiva spellistan (ej frĂ„n huvudbiblioteket "Musik"). + */ + private void removeSelectedSong() { + DisplaySong sel = songTable.getSelectionModel().getSelectedItem(); + String list = sourceList.getSelectionModel().getSelectedItem(); + // Skydd: Man fĂ„r inte ta bort lĂ„tar direkt frĂ„n "Musik"-biblioteket i denna vy + if (sel != null && list != null && !list.equals("Musik")) { + allPlaylists.get(list).remove(sel); + } + } + + /** + * Visar en popup-meny för att lĂ€gga till vald lĂ„t i en annan spellista. + */ + private void addSelectedSong(Button anchor) { + DisplaySong sel = songTable.getSelectionModel().getSelectedItem(); + if (sel == null) return; // Ingen lĂ„t vald + + ContextMenu menu = new ContextMenu(); + for (String n : playlistNames) { + if (n.equals("Musik")) continue; // Man kan inte lĂ€gga till i "Musik" (det Ă€r kĂ€llan) + + MenuItem itm = new MenuItem(n); + itm.setOnAction(e -> { + // Om lĂ„ten inte redan finns i listan, lĂ€gg till den + if (!allPlaylists.get(n).contains(sel)) { + allPlaylists.get(n).add(sel); + } + }); + menu.getItems().add(itm); + } + // Visa menyn vid knappen + menu.show(anchor, anchor.getScene().getWindow().getX() + anchor.getLayoutX(), anchor.getScene().getWindow().getY() + anchor.getLayoutY()); + } + + /** + * En inre klass (DTO - Data Transfer Object) enbart för visning i tabellen. + * Detta skiljer GUI-logiken frĂ„n databas-entiteterna. + */ + public static class DisplaySong { + String name, artist, album, time; + public DisplaySong(String n, String a, String al, Long t) { + this.name = n; + this.artist = a; + this.album = al; + this.time = String.valueOf(t); + } + } +} diff --git a/src/main/java/org/example/MyPod.java b/src/main/java/org/example/MyPod.java new file mode 100644 index 00000000..5500942e --- /dev/null +++ b/src/main/java/org/example/MyPod.java @@ -0,0 +1,374 @@ +package org.example; + +import jakarta.persistence.EntityManagerFactory; +import javafx.concurrent.Task; +import javafx.application.Platform; +import javafx.application.Application; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Rectangle; +import javafx.stage.Stage; +import org.example.entity.Album; +import org.example.entity.Artist; +import org.example.entity.Song; +import org.example.repo.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * Huvudklassen för applikationen "MyPod". + * Denna klass bygger upp GUI:t (simulerar en iPod) och hanterar navigering. + */ +public class MyPod extends Application { + + // --- DATA-LAGER --- + // Repositories anvĂ€nds för att hĂ€mta data frĂ„n databasen istĂ€llet för att hĂ„rdkoda den. + private final SongRepository songRepo = new SongRepositoryImpl(); + private final ArtistRepository artistRepo = new ArtistRepositoryImpl(); + private final AlbumRepository albumRepo = new AlbumRepositoryImpl(); + private final ItunesApiClient apiClient = new ItunesApiClient(); + + // Listor som hĂ„ller datan vi hĂ€mtat frĂ„n databasen + private List songs; + private List artists; + private List albums; + + // --- MENY-DATA --- + // Huvudmenyns alternativ. "ObservableList" Ă€r en speciell lista i JavaFX + // som GUI:t kan "lyssna" pĂ„, Ă€ven om vi hĂ€r mest anvĂ€nder den som en vanlig lista. + private final ObservableList mainMenu = FXCollections.observableArrayList( + "Songs", "Artists", "Albums", "Playlists"); + + // En lista med sjĂ€lva Label-objekten som visas pĂ„ skĂ€rmen (för att kunna markera/avmarkera dem) + private final List