diff --git a/.gitignore b/.gitignore
index 275ff638..62d8dd17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,4 +39,14 @@ build/
.DS_Store
### other files ###
-users.csv
\ No newline at end of file
+users.csv
+
+# Environment variables
+.env
+.env.local
+.env.development
+.env.production
+
+# Configuration files with secrets
+config.properties
+*.config
\ No newline at end of file
diff --git a/README.md b/README.md
index 9487008b..85e9832f 100644
--- a/README.md
+++ b/README.md
@@ -1,252 +1,10 @@
-# Clean Architecture Team Lab Activity: Login and Logout
+Cleaned out this readme. Add stuff here
-In this team lab activity, your team will:
-- explore an existing use case (login)
-- add a new use case (logout).
+Original readme from here:
+https://github.com/CSC207-2025F-UofT/ca-lab/blob/main/README.md
-To earn credit:
-- your team must demo your working `logout` use case.
+Also, Spotify WebAPi Java wrapper in case we want to use it.
+https://github.com/spotify-web-api-java/spotify-web-api-java?tab=readme-ov-file
-Your demo should be similar to the below example:
+> import se.michaelthelin.spotify.SpotifyApi; to access the api
-
-
----
-
-## Task 0: Fork this repo on GitHub
-**To get started, one team member should fork this repo on GitHub and share it with the team.
-All team members should then clone it.**
-
-
-**Suggested logistics:** One of you should invite the others to collaborate on their fork of the original repo on GitHub. You can do this in your repo on GitHub under `Settings -> Collaborators`. This will allow you to push branches to a common repo and then use pull requests to contribute your code and review. To prevent others from pushing directly to the main branch, we recommend that you set branch protection rules on GitHub. Below are how the settings might look if you add branch protection rules:
-
-
-
----
-
-## Task 1: Understanding the Program
-
-Open the project in IntelliJ. Open `app.Main` and read it as a team.
-- What are the currently implemented Views and Use Cases in the program?
-- Which Use Cases are triggered from each View?
-- Which version of the DAO is `app.Main` using?
-
-> Observe that the main method makes use of the `app.AppBuilder` class which
-is responsible for constructing our CA engine for each use case of the application. To answer the last two questions above, you will need to look inside the details of the `app.AppBuilder` class.
-
-**Make sure that each member of your team can successfully run `app/Main.java`.**
-- Ensure that you are each able to create a new user and log in using the username and password.
-
-> Note: you may need to set the Project SDK in the `Project Structure...` menu, and possibly
-> also manually link the Maven project if the app won't run when you try to run Main.
-
-### Task 1.1: Exploring the login use case
-
-Let's take a tour of the login use case code:
-
-- In IntelliJ, find the `LoginController` class and open it.
-
-- Set a breakpoint inside its `execute` method.
-
-- Run the program in debug mode.
-
-- On the login page, attempt to log in with an existing account. When you click the button, the breakpoint that you set will be triggered.
-
-- **Step through the code to trace the execution of the login use case.**
- Importantly, pay extra close attention to what the Presenter does to ensure that the LoggedInView gets displayed after the user successfully gets logged into the application.
-
-The code is designed based on our CA Engine that was introduced in the reading this week. For reference, here is our CA Engine diagram:
-
-
-
-> Pay attention to the classes involved and the flow of execution. When your team implements the logout use case next, your code will need to have a very similar structure.
-
-To better understand how the view gets updated, your team may find it useful to review the [Extra Advice about the Presenters, Views, and ViewModels](#extra-advice-about-the-presenters-views-and-viewmodels) section at the end of this README.
-
-## Task 2: Implementing the Logout Use Case
-
-Currently, you'll notice that the "Log Out" button in the `LoggedInView` still doesn't actually log you out of the program. Let's fix this.
-
-We have created all the classes for your team, but some of the code is missing. **As a team, your task is to fill in the missing code so that the logout use case is functional.**
-
-> The next part of the readme describes how your team will do this.
-
-Your team will know when you are done when:
-
-- Clicking the "Log Out" button takes the user back to the Login View when you use the program.
-- On the Login View, the username of the logged-out user is filled in.
-- The provided `LogoutInteractorTest` test passes.
-
-### Task 2.1: Dividing up the work
-
-There are `TODO` comments left in the files.
-
-> Recall that you can use the TODO tool window to conveniently pull up a complete list.
-
-Once all TODOs are complete, the "Log Out" button _should_ work!
-
-**As a team, split up the TODOs (see below) between the members of your team.**
-
-> Optionally, your team can make GitHub Issues and assign them to each team member.
-
-Make sure that each member has at least one TODO that they will be responsible for completing.
-If your team prefers to work in pairs, that is fine too.
-
-The TODOs are summarized below (by file) to help your team decide how to split them up:
-
----
-
-- `Main.java` (tip: look at how other use cases have been added)
- -[ ] TODO: add the logout use case to the app
-
----
-
-- `LoggedInView.java` (tip: refer to the other views for similar code)
- -[ ] TODO: save the logout controller in the instance variable.
- -[ ] TODO: execute the logout use case through the Controller
-
----
-
-- `LogoutController.java` (tip: refer to the other controllers for similar code)
- -[ ] TODO: Save the interactor in the instance variable.
- -[ ] TODO: run the use case interactor for the logout use case
-
-> Note: there is no input data necessary for this use case.
-
----
-
-- `LogoutInteractor.java` (tip: refer to `ChangePasswordInteractor.java` for similar code)
- -[ ] TODO: save the DAO and Presenter in the instance variables.
- -[ ] TODO: implement the logic of the Logout Use Case
-
-> Note: there is no input data necessary for this use case.
-
----
-
-- `LogoutPresenter.java` (tip: refer to `SignupPresenter.java` for similar code)
- -[ ] TODO: assign to the three instance variables.
- -[ ] TODO: have prepareSuccessView update the LoggedInState
- -[ ] TODO: have prepareSuccessView update the LoginState
-
----
-
-### Task 2.2: Complete your TODOs!
-With the work divided up, your team should complete the TODOs through a sequence of PRs.
-
-1. Make a branch for your work.
-
-> Make sure that you switch to your new branch!
-
-2. Complete your assigned TODO and make a pull request on GitHub. In your pull request,
- briefly describe what your TODO was and how you implemented it. If you aren't sure
- about part of it, include this in your pull request so that everyone knows what to look
- for when reviewing — or you can of course discuss with your team before making your
- pull request.
-
-3. Review all pull requests to ensure each TODO is correctly implemented.
-
-4. Once all TODOs are completed, your team should debug as needed to ensure the
- correctness of the code. Setting a breakpoint where the logout use case
- interactor starts its work will likely be a great place to start when debugging.
-
-And that's it; your team should now have a working logout use case!
-
-**Demo your working code to your TA to earn credit.**
-
----
-
-# Extra Advice about the Presenters, Views, and ViewModels
-
-One of the trickiest parts of the code will be the flow of information between these pieces of the program. Below briefly explains how these pieces fit together and work in the context of the login use case.
-
-## ViewModels and States
-
-In the design of this program, `ViewModel` is written using generics to allow
-for different "state" objects to be stored. For a `LoginViewModel`, the state is an instance of class `LoginState`. Each state object will just contain a basic constructor, getters, and setters to store the data of the view model.
-
-## A View and its ViewModel
-
-In the constructor of `LoginView`, the following line of code connects this instance of `LoginView` to its associated `LoginViewModel`:
-
-```java
-this.loginViewModel.addPropertyChangeListener(this);
-```
-
-This should remind you of the code we write when adding an action listener to a button. The code is following the same structure.
-
-> We'll talk more about this "pattern" of _events_ and _listeners_ in our next module.
-
-When the presenter updates the view model later, an event will be triggered — resulting in the view's `propertyChange` method getting called, with a `PropertyChangeEvent` object being passed through as the argument to the call.
-
-For example, the `LoginView.propertyChange` method looks like:
-
-```java
-public void propertyChange(PropertyChangeEvent evt) {
- final LoginState state = (LoginState) evt.getNewValue();
- setFields(state);
- usernameErrorField.setText(state.getLoginError());
- }
-```
-
-The `LoginView` gets the `LoginState` object stored in the `LoginViewModel` and updates itself with that information.
-
-## A Presenter and its ViewModel(s)
-
-A presenter may have one or more view models associated with it. For example, the login use case's presenter has a reference to a `LoginViewModel` and a `LoggedInViewModel`, since it will need to update both view models. Additionally, our implementation makes use of a `ViewManager` and `ViewManagerModel` to keep track of which view
-the user should currently see.
-
-Let's take a look at the `LoginPresenter.prepareSuccessView` method as an example:
-
-```java
-public void prepareSuccessView(LoginOutputData response) {
- // On success, update the loggedInViewModel's state
- final LoggedInState loggedInState = loggedInViewModel.getState();
- loggedInState.setUsername(response.getUsername());
- this.loggedInViewModel.firePropertyChanged();
-
- // and clear everything from the LoginViewModel's state
- this.loginViewModel.setState(new LoginState());
- this.loginViewModel.firePropertyChanged();
-
- // switch to the logged in view
- this.viewManagerModel.setState(loggedInViewModel.getViewName());
- this.viewManagerModel.firePropertyChanged();
-}
-```
-
-The first part of the code updates the view model for the logged-in view so that the newly logged-in username will be displayed. Once the state is updated, the `firePropertyChanged` method is called, which is what will trigger the call to the view's `propertyChange` method which will update the view based on the updated view model.
-
-This can be visualized as a sequence diagram as follows:
-
-> Note: this diagram has been simplified to focus on the high-level flow of information; the actual stack trace includes some additional intermediate calls which you can see if you step through the code in the debugger or manually click through the code.
-
-
-
-We then do the same, but for the login view model whose state we want to clear.
-
-Lastly, we update the state of the `viewManagerModel`, and alert the viewManager that it should switch to displaying the logged-in view.
-
-> Setting a breakpoint in the code and stepping through can help you see how the information flows through the system. Pay attention to the contents of the call stack to help you track where you are in the execution of the use case.
-
-## The ViewManager
-
-This class may stand out as a bit unclear about how it fits into our architecture, as it isn't in the CA Engine diagram at all. Remember that the CA Engine is representing a single use case in our program. Once our program has _multiple_ use cases, we naturally need some kind of additional code to connect them together. As we have seen, one use case can lead to a change in which view is presented to the user. To facilitate this, our implementation used a `ViewManager` and associated `ViewManagerModel` to take care of this switching for us. The state of a `ViewManagerModel` object is simply a string that indicates the name of the currently visible view (`JPanel` in this implementation). The `ViewManager` uses a `CardLayout` to conveniently display only the currently active view at a given time.
-
-When the `ViewManager` is alerted of a change to its associated `ViewManagerModel`, its `propertyChange` method is executed:
-
-```java
-public void propertyChange(PropertyChangeEvent evt) {
- if (evt.getPropertyName().equals("state")) {
- final String viewModelName = (String) evt.getNewValue();
- cardLayout.show(views, viewModelName);
- }
- }
-```
-
-This code will update the application to display the view corresponding to the `viewModelName` string.
-
-> In the `AppBuilder` code, you can see how the views are originally added to the `cardLayout`.
-
-> Thought Question: Can you think of any alternatives to our `ViewManager` implementation for managing multiple views?
-
----
diff --git a/loyaltyscore_1203.json b/loyaltyscore_1203.json
new file mode 100644
index 00000000..c346b359
--- /dev/null
+++ b/loyaltyscore_1203.json
@@ -0,0 +1,272 @@
+{"loyalty_scores": [
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ }
+]}
\ No newline at end of file
diff --git a/loyaltyscore_520.json b/loyaltyscore_520.json
new file mode 100644
index 00000000..01d08aed
--- /dev/null
+++ b/loyaltyscore_520.json
@@ -0,0 +1,12 @@
+{"loyalty_scores": [
+ {
+ "date": "2023-11-23",
+ "score": 85,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2024-11-25",
+ "score": 150,
+ "artist_name": "Artist1"
+ }
+]}
\ No newline at end of file
diff --git a/loyaltyscore_kh1203.json b/loyaltyscore_kh1203.json
new file mode 100644
index 00000000..271f2fd1
--- /dev/null
+++ b/loyaltyscore_kh1203.json
@@ -0,0 +1,92 @@
+{"loyalty_scores": [
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ },
+ {
+ "date": "2025-04-12",
+ "score": 90,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-04-13",
+ "score": 590,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 620,
+ "artist_name": "Artist1"
+ },
+ {
+ "date": "2025-11-24",
+ "score": 1130,
+ "artist_name": "Artist2"
+ }
+]}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 5875d5ae..4565a785 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
okhttp4.12.0
+
org.junit.jupiterjunit-jupiter
@@ -28,6 +29,19 @@
test
+
+ se.michaelthelin.spotify
+ spotify-web-api-java
+ 9.4.0
+
+
+
+ org.mockito
+ mockito-core
+ 4.5.1
+ test
+
+
@@ -49,4 +63,4 @@
UTF-8
-
\ No newline at end of file
+
diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java
index 3c654cad..0c46832f 100644
--- a/src/main/java/app/AppBuilder.java
+++ b/src/main/java/app/AppBuilder.java
@@ -1,34 +1,35 @@
package app;
import data_access.FileUserDataAccessObject;
+import data_access.SpotifyDataAccessObject;
import entity.UserFactory;
import interface_adapter.ViewManagerModel;
-import interface_adapter.logged_in.ChangePasswordController;
-import interface_adapter.logged_in.ChangePasswordPresenter;
import interface_adapter.logged_in.LoggedInViewModel;
-import interface_adapter.login.LoginController;
import interface_adapter.login.LoginPresenter;
import interface_adapter.login.LoginViewModel;
-import interface_adapter.logout.LogoutController;
-import interface_adapter.logout.LogoutPresenter;
-import interface_adapter.signup.SignupController;
-import interface_adapter.signup.SignupPresenter;
-import interface_adapter.signup.SignupViewModel;
+import interface_adapter.spotify_auth.SpotifyAuthController;
+import interface_adapter.spotify_auth.SpotifyAuthPresenter;
+import interface_adapter.spotify_auth.SpotifyAuthViewModel;
+import interface_adapter.daily_mix.DailyMixViewModel;
+import interface_adapter.daily_mix.DailyMixController;
+import interface_adapter.daily_mix.DailyMixPresenter;
import use_case.change_password.ChangePasswordInputBoundary;
import use_case.change_password.ChangePasswordInteractor;
import use_case.change_password.ChangePasswordOutputBoundary;
import use_case.login.LoginInputBoundary;
import use_case.login.LoginInteractor;
import use_case.login.LoginOutputBoundary;
-import use_case.logout.LogoutInputBoundary;
-import use_case.logout.LogoutInteractor;
-import use_case.logout.LogoutOutputBoundary;
-import use_case.signup.SignupInputBoundary;
-import use_case.signup.SignupInteractor;
-import use_case.signup.SignupOutputBoundary;
+import use_case.spotify_auth.SpotifyAuthInputBoundary;
+import use_case.spotify_auth.SpotifyAuthInteractor;
+import use_case.spotify_auth.SpotifyAuthOutputBoundary;
+import use_case.daily_mix.DailyMixInputBoundary;
+import use_case.daily_mix.DailyMixInputData;
+import use_case.daily_mix.DailyMixInteractor;
+import use_case.daily_mix.DailyMixOutputBoundary;
+import use_case.daily_mix.DailyMixOutputData;
import view.LoggedInView;
import view.LoginView;
-import view.SignupView;
+import view.SpotifyAuthView;
import view.ViewManager;
import javax.swing.*;
@@ -37,112 +38,120 @@
public class AppBuilder {
private final JPanel cardPanel = new JPanel();
private final CardLayout cardLayout = new CardLayout();
- final UserFactory userFactory = new UserFactory();
final ViewManagerModel viewManagerModel = new ViewManagerModel();
ViewManager viewManager = new ViewManager(cardPanel, cardLayout, viewManagerModel);
- // set which data access implementation to use, can be any
- // of the classes from the data_access package
-
- // DAO version using local file storage
- final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory);
-
- // DAO version using a shared external database
- // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory);
-
- private SignupView signupView;
- private SignupViewModel signupViewModel;
private LoginViewModel loginViewModel;
private LoggedInViewModel loggedInViewModel;
private LoggedInView loggedInView;
private LoginView loginView;
+ private SpotifyAuthView spotifyAuthView;
+ private SpotifyAuthViewModel spotifyAuthViewModel;
+ private DailyMixViewModel dailyMixViewModel;
public AppBuilder() {
cardPanel.setLayout(cardLayout);
}
- public AppBuilder addSignupView() {
- signupViewModel = new SignupViewModel();
- signupView = new SignupView(signupViewModel);
- cardPanel.add(signupView, signupView.getViewName());
+ public AppBuilder addViewModels() {
+ loginViewModel = new LoginViewModel();
+ loggedInViewModel = new LoggedInViewModel();
+ spotifyAuthViewModel = new SpotifyAuthViewModel();
+ dailyMixViewModel = new DailyMixViewModel();
return this;
}
public AppBuilder addLoginView() {
- loginViewModel = new LoginViewModel();
- loginView = new LoginView(loginViewModel);
+ loginView = new LoginView(loginViewModel, viewManagerModel);
cardPanel.add(loginView, loginView.getViewName());
return this;
}
public AppBuilder addLoggedInView() {
- loggedInViewModel = new LoggedInViewModel();
- loggedInView = new LoggedInView(loggedInViewModel);
+ loggedInView = new LoggedInView(loggedInViewModel, viewManagerModel, spotifyAuthViewModel,dailyMixViewModel);
cardPanel.add(loggedInView, loggedInView.getViewName());
return this;
}
- public AppBuilder addSignupUseCase() {
- final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel,
- signupViewModel, loginViewModel);
- final SignupInputBoundary userSignupInteractor = new SignupInteractor(
- userDataAccessObject, signupOutputBoundary, userFactory);
+ public AppBuilder addSpotifyAuthView() {
+ spotifyAuthView = new SpotifyAuthView(spotifyAuthViewModel, loggedInViewModel); // Pass loggedInViewModel
+ spotifyAuthView.setViewManagerModel(viewManagerModel);
+ cardPanel.add(spotifyAuthView, spotifyAuthView.getViewName());
+ return this;
+ }
- SignupController controller = new SignupController(userSignupInteractor);
- signupView.setSignupController(controller);
+ public AppBuilder addSignupUseCase() {
+ // emptied; do not need
return this;
}
public AppBuilder addLoginUseCase() {
final LoginOutputBoundary loginOutputBoundary = new LoginPresenter(viewManagerModel,
loggedInViewModel, loginViewModel);
- final LoginInputBoundary loginInteractor = new LoginInteractor(
- userDataAccessObject, loginOutputBoundary);
+ // final LoginInputBoundary loginInteractor = new LoginInteractor(
+ // userDataAccessObject, loginOutputBoundary);
+ // TODO: add back in LoginInputBoundary loginInteractor variable
- LoginController loginController = new LoginController(loginInteractor);
- loginView.setLoginController(loginController);
+
+ // LoginController loginController = new LoginController(loginInteractor);
+ // TODO: Add back in LoginController
+ // loginView.setLoginController(loginController);
return this;
}
public AppBuilder addChangePasswordUseCase() {
- final ChangePasswordOutputBoundary changePasswordOutputBoundary = new ChangePasswordPresenter(viewManagerModel,
- loggedInViewModel);
+ // emptied, do not need.
+ return this;
+ }
- final ChangePasswordInputBoundary changePasswordInteractor =
- new ChangePasswordInteractor(userDataAccessObject, changePasswordOutputBoundary, userFactory);
+ public AppBuilder addLogoutUseCase() {
+ // emptied; not necessary.
+ return this;
+ }
+
+ public AppBuilder addSpotifyAuthUseCase() {
+ // PKCE doesn't need client secret!
+ data_access.SpotifyDataAccessObject spotifyDAO = new data_access.SpotifyDataAccessObject();
+
+ final SpotifyAuthOutputBoundary spotifyAuthOutputBoundary =
+ new SpotifyAuthPresenter(viewManagerModel, spotifyAuthViewModel, loggedInViewModel);
+
+ if (spotifyAuthOutputBoundary instanceof SpotifyAuthPresenter) {
+ ((SpotifyAuthPresenter) spotifyAuthOutputBoundary).setLoggedInView(loggedInView);
+ }
+
+ final SpotifyAuthInputBoundary spotifyAuthInteractor =
+ new SpotifyAuthInteractor(spotifyDAO, spotifyAuthOutputBoundary);
+
+ SpotifyAuthController controller = new SpotifyAuthController(spotifyAuthInteractor);
+ spotifyAuthView.setSpotifyAuthController(controller);
- ChangePasswordController changePasswordController = new ChangePasswordController(changePasswordInteractor);
- loggedInView.setChangePasswordController(changePasswordController);
return this;
}
- /**
- * Adds the Logout Use Case to the application.
- * @return this builder
- */
- public AppBuilder addLogoutUseCase() {
- final LogoutOutputBoundary logoutOutputBoundary = new LogoutPresenter(viewManagerModel,
- loggedInViewModel, loginViewModel);
+ public AppBuilder addDailyMixUseCase() {
+ // use new DailyMixPresenter and Interactor
+ final DailyMixOutputBoundary dailyMixOutputBoundary =
+ new DailyMixPresenter(dailyMixViewModel);
- final LogoutInputBoundary logoutInteractor =
- new LogoutInteractor(userDataAccessObject, logoutOutputBoundary);
+ final DailyMixInputBoundary dailyMixInteractor =
+ new DailyMixInteractor(new SpotifyDataAccessObject(), dailyMixOutputBoundary);
+
+ DailyMixController dailyMixController = new DailyMixController(dailyMixInteractor);
+ loggedInView.setDailyMixController(dailyMixController);
- final LogoutController logoutController = new LogoutController(logoutInteractor);
- loggedInView.setLogoutController(logoutController);
return this;
}
public JFrame build() {
- final JFrame application = new JFrame("User Login Example");
+ final JFrame application = new JFrame("Better Wrapped - Spotify Analysis");
application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
application.add(cardPanel);
- viewManagerModel.setState(signupView.getViewName());
+ viewManagerModel.setState(loginView.getViewName());
viewManagerModel.firePropertyChange();
return application;
}
-
-
}
diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java
index 424404fb..06df2fcb 100644
--- a/src/main/java/app/Main.java
+++ b/src/main/java/app/Main.java
@@ -3,19 +3,36 @@
import javax.swing.*;
public class Main {
+ // VARIABLES CLIENT_ID AND CLIENT_SECRET MUST BE SET:
+
+ String CLIENT_ID;
+ String CLIENT_SECRET;
+
public static void main(String[] args) {
+ String clientId = System.getenv("SPOTIFY_CLIENT_ID");
+
+ if (clientId == null) {
+ System.out.println("Please set SPOTIFY_CLIENT_ID environment variable");
+ System.out.println("You can get this from: https://developer.spotify.com/dashboard");
+ System.out.println("Note: PKCE flow doesn't require a client secret!");
+ System.out.println("Running without Spotify integration...");
+ }
+
AppBuilder appBuilder = new AppBuilder();
JFrame application = appBuilder
+ .addViewModels() // ✅ Create ALL view models FIRST
.addLoginView()
- .addSignupView()
.addLoggedInView()
+ .addSpotifyAuthView()
.addSignupUseCase()
.addLoginUseCase()
.addChangePasswordUseCase()
+ .addLogoutUseCase()
+ .addSpotifyAuthUseCase()
+ .addDailyMixUseCase()
.build();
-
application.pack();
application.setLocationRelativeTo(null);
application.setVisible(true);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/data_access/DBUserDataAccessObject.java b/src/main/java/data_access/DBUserDataAccessObject.java
deleted file mode 100644
index 08cc04ad..00000000
--- a/src/main/java/data_access/DBUserDataAccessObject.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package data_access;
-
-import entity.User;
-import entity.UserFactory;
-import okhttp3.*;
-import org.json.JSONException;
-import org.json.JSONObject;
-import use_case.change_password.ChangePasswordUserDataAccessInterface;
-import use_case.login.LoginUserDataAccessInterface;
-import use_case.logout.LogoutUserDataAccessInterface;
-import use_case.signup.SignupUserDataAccessInterface;
-
-import java.io.IOException;
-
-/**
- * The DAO for user data.
- */
-public class DBUserDataAccessObject implements SignupUserDataAccessInterface,
- LoginUserDataAccessInterface,
- ChangePasswordUserDataAccessInterface,
- LogoutUserDataAccessInterface {
- private static final int SUCCESS_CODE = 200;
- private static final String CONTENT_TYPE_LABEL = "Content-Type";
- private static final String CONTENT_TYPE_JSON = "application/json";
- private static final String STATUS_CODE_LABEL = "status_code";
- private static final String USERNAME = "username";
- private static final String PASSWORD = "password";
- private static final String MESSAGE = "message";
- private final UserFactory userFactory;
-
- private String currentUsername;
-
- public DBUserDataAccessObject(UserFactory userFactory) {
- this.userFactory = userFactory;
- }
-
- @Override
- public User get(String username) {
- // Make an API call to get the user object.
- final OkHttpClient client = new OkHttpClient().newBuilder().build();
- final Request request = new Request.Builder()
- .url(String.format("http://vm003.teach.cs.toronto.edu:20112/user?username=%s", username))
- .addHeader("Content-Type", CONTENT_TYPE_JSON)
- .build();
- try {
- final Response response = client.newCall(request).execute();
-
- final JSONObject responseBody = new JSONObject(response.body().string());
-
- if (responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE) {
- final JSONObject userJSONObject = responseBody.getJSONObject("user");
- final String name = userJSONObject.getString(USERNAME);
- final String password = userJSONObject.getString(PASSWORD);
-
- return userFactory.create(name, password);
- }
- else {
- throw new RuntimeException(responseBody.getString(MESSAGE));
- }
- }
- catch (IOException | JSONException ex) {
- throw new RuntimeException(ex);
- }
- }
-
- @Override
- public void setCurrentUsername(String name) {
- currentUsername = name;
- }
-
- @Override
- public String getCurrentUsername() {
- return currentUsername;
- }
-
- @Override
- public boolean existsByName(String username) {
- final OkHttpClient client = new OkHttpClient().newBuilder()
- .build();
- final Request request = new Request.Builder()
- .url(String.format("http://vm003.teach.cs.toronto.edu:20112/checkIfUserExists?username=%s", username))
- .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_JSON)
- .build();
- try {
- final Response response = client.newCall(request).execute();
-
- final JSONObject responseBody = new JSONObject(response.body().string());
-
- // throw new RuntimeException(responseBody.getString("message"));
- return responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE;
- }
- catch (IOException | JSONException ex) {
- throw new RuntimeException(ex);
- }
- }
-
- @Override
- public void save(User user) {
- final OkHttpClient client = new OkHttpClient().newBuilder()
- .build();
-
- // POST METHOD
- final MediaType mediaType = MediaType.parse(CONTENT_TYPE_JSON);
- final JSONObject requestBody = new JSONObject();
- requestBody.put(USERNAME, user.getName());
- requestBody.put(PASSWORD, user.getPassword());
- final RequestBody body = RequestBody.create(requestBody.toString(), mediaType);
- final Request request = new Request.Builder()
- .url("http://vm003.teach.cs.toronto.edu:20112/user")
- .method("POST", body)
- .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_JSON)
- .build();
- try {
- final Response response = client.newCall(request).execute();
-
- final JSONObject responseBody = new JSONObject(response.body().string());
-
- if (responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE) {
- // success!
- }
- else {
- throw new RuntimeException(responseBody.getString(MESSAGE));
- }
- }
- catch (IOException | JSONException ex) {
- throw new RuntimeException(ex);
- }
- }
-
- @Override
- public void changePassword(User user) {
- final OkHttpClient client = new OkHttpClient().newBuilder()
- .build();
-
- // POST METHOD
- final MediaType mediaType = MediaType.parse(CONTENT_TYPE_JSON);
- final JSONObject requestBody = new JSONObject();
- requestBody.put(USERNAME, user.getName());
- requestBody.put(PASSWORD, user.getPassword());
- final RequestBody body = RequestBody.create(requestBody.toString(), mediaType);
- final Request request = new Request.Builder()
- .url("http://vm003.teach.cs.toronto.edu:20112/user")
- .method("PUT", body)
- .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_JSON)
- .build();
- try {
- final Response response = client.newCall(request).execute();
-
- final JSONObject responseBody = new JSONObject(response.body().string());
-
- if (responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE) {
- // success!
- }
- else {
- throw new RuntimeException(responseBody.getString(MESSAGE));
- }
- }
- catch (IOException | JSONException ex) {
- throw new RuntimeException(ex);
- }
- }
-}
diff --git a/src/main/java/data_access/FileUserDataAccessObject.java b/src/main/java/data_access/FileUserDataAccessObject.java
deleted file mode 100644
index 4cae88c5..00000000
--- a/src/main/java/data_access/FileUserDataAccessObject.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package data_access;
-
-import entity.User;
-import entity.UserFactory;
-import use_case.change_password.ChangePasswordUserDataAccessInterface;
-import use_case.login.LoginUserDataAccessInterface;
-import use_case.logout.LogoutUserDataAccessInterface;
-import use_case.signup.SignupUserDataAccessInterface;
-
-import java.io.*;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * DAO for user data implemented using a File to persist the data.
- */
-public class FileUserDataAccessObject implements SignupUserDataAccessInterface,
- LoginUserDataAccessInterface,
- ChangePasswordUserDataAccessInterface,
- LogoutUserDataAccessInterface {
-
- private static final String HEADER = "username,password";
-
- private final File csvFile;
- private final Map headers = new LinkedHashMap<>();
- private final Map accounts = new HashMap<>();
-
- private String currentUsername;
-
- /**
- * Construct this DAO for saving to and reading from a local file.
- * @param csvPath the path of the file to save to
- * @param userFactory factory for creating user objects
- * @throws RuntimeException if there is an IOException when accessing the file
- */
- public FileUserDataAccessObject(String csvPath, UserFactory userFactory) {
-
- csvFile = new File(csvPath);
- headers.put("username", 0);
- headers.put("password", 1);
-
- if (csvFile.length() == 0) {
- save();
- }
- else {
-
- try (BufferedReader reader = new BufferedReader(new FileReader(csvFile))) {
- final String header = reader.readLine();
-
- if (!header.equals(HEADER)) {
- throw new RuntimeException(String.format("header should be%n: %s%n but was:%n%s", HEADER, header));
- }
-
- String row;
- while ((row = reader.readLine()) != null) {
- final String[] col = row.split(",");
- final String username = String.valueOf(col[headers.get("username")]);
- final String password = String.valueOf(col[headers.get("password")]);
- final User user = userFactory.create(username, password);
- accounts.put(username, user);
- }
- }
- catch (IOException ex) {
- throw new RuntimeException(ex);
- }
- }
- }
-
- private void save() {
- final BufferedWriter writer;
- try {
- writer = new BufferedWriter(new FileWriter(csvFile));
- writer.write(String.join(",", headers.keySet()));
- writer.newLine();
-
- for (User user : accounts.values()) {
- final String line = String.format("%s,%s",
- user.getName(), user.getPassword());
- writer.write(line);
- writer.newLine();
- }
-
- writer.close();
-
- }
- catch (IOException ex) {
- throw new RuntimeException(ex);
- }
- }
-
- @Override
- public void save(User user) {
- accounts.put(user.getName(), user);
- this.save();
- }
-
- @Override
- public User get(String username) {
- return accounts.get(username);
- }
-
- @Override
- public void setCurrentUsername(String name) {
- currentUsername = name;
- }
-
- @Override
- public String getCurrentUsername() {
- return currentUsername;
- }
-
- @Override
- public boolean existsByName(String identifier) {
- return accounts.containsKey(identifier);
- }
-
- @Override
- public void changePassword(User user) {
- // Replace the User object in the map
- accounts.put(user.getName(), user);
- save();
- }
-}
diff --git a/src/main/java/data_access/InMemoryUserDataAccessObject.java b/src/main/java/data_access/InMemoryUserDataAccessObject.java
deleted file mode 100644
index fc5dd2b2..00000000
--- a/src/main/java/data_access/InMemoryUserDataAccessObject.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package data_access;
-
-import entity.User;
-import use_case.change_password.ChangePasswordUserDataAccessInterface;
-import use_case.login.LoginUserDataAccessInterface;
-import use_case.logout.LogoutUserDataAccessInterface;
-import use_case.signup.SignupUserDataAccessInterface;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * In-memory implementation of the DAO for storing user data. This implementation does
- * NOT persist data between runs of the program.
- */
-public class InMemoryUserDataAccessObject implements SignupUserDataAccessInterface,
- LoginUserDataAccessInterface,
- ChangePasswordUserDataAccessInterface,
- LogoutUserDataAccessInterface {
-
- private final Map users = new HashMap<>();
-
- private String currentUsername;
-
- @Override
- public boolean existsByName(String identifier) {
- return users.containsKey(identifier);
- }
-
- @Override
- public void save(User user) {
- users.put(user.getName(), user);
- }
-
- @Override
- public User get(String username) {
- return users.get(username);
- }
-
- @Override
- public void setCurrentUsername(String name) {
- currentUsername = name;
- }
-
- @Override
- public String getCurrentUsername() {
- return currentUsername;
- }
-
- @Override
- public void changePassword(User user) {
- // Replace the old entry with the new password
- users.put(user.getName(), user);
- }
-
-}
diff --git a/src/main/java/data_access/LoyaltyScoreDataAccessObject.java b/src/main/java/data_access/LoyaltyScoreDataAccessObject.java
new file mode 100644
index 00000000..d6936e61
--- /dev/null
+++ b/src/main/java/data_access/LoyaltyScoreDataAccessObject.java
@@ -0,0 +1,167 @@
+package data_access;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import use_case.loyalty_score.LoyaltyScoreDataAccessInterface;
+
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of LoyaltyScoreDataAccessInterface
+ * The file is formatted as: loyaltyscore_userid
+ * {
+ * "loyalty_scores": [
+ * {
+ * "date": "2023-11-23",
+ * "artist_name": "Artist1",
+ * "score": 85
+ * }
+ */
+
+public class LoyaltyScoreDataAccessObject implements LoyaltyScoreDataAccessInterface {
+
+
+ @Override
+ public Map getLoyaltyDate(String userid, String date) {
+ Map loyaltyScores = new HashMap<>();
+ JSONObject rootNode = readUserFile(userid);
+ if (rootNode == null) {
+ return loyaltyScores; // Early return if the file doesn't exist
+ }
+
+ JSONArray loyaltyScoresArray = rootNode.optJSONArray("loyalty_scores");
+ if (loyaltyScoresArray == null) {
+ return loyaltyScores; // Early return if the loyalty_scores array doesn't exist
+ }
+
+ loyaltyScoresArray.forEach(entry -> {
+ JSONObject loyaltyScore = (JSONObject) entry;
+ if (loyaltyScore.getString("date").equals(date)) {
+ String artistName = loyaltyScore.getString("artist_name");
+ int score = loyaltyScore.getInt("score");
+ loyaltyScores.put(artistName, score);
+ }
+ });
+ return loyaltyScores;
+ }
+
+ @Override
+ public boolean loyaltyScoreExists(String userid, String date, String artist_name) {
+ JSONObject rootNode = readUserFile(userid);
+ if (rootNode == null) {
+ return false; // Early return if the file doesn't exist
+ }
+
+ JSONArray loyaltyScoresArray = rootNode.optJSONArray("loyalty_scores");
+ if (loyaltyScoresArray == null) {
+ return false; // Early return if the loyalty_scores array doesn't exist
+ }
+
+ for (Object entry : loyaltyScoresArray) {
+ JSONObject loyaltyScore = (JSONObject) entry;
+ if (loyaltyScore.getString("date").equals(date) &&
+ loyaltyScore.getString("artist_name").equals(artist_name)) {
+ return true; // Return true as soon as we find a match
+ }
+ }
+ return false; // No match found
+ }
+
+ @Override
+ public void saveLoyalty(String userid, String date, String artist_name, int score) {
+ JSONObject rootNode = readUserFile(userid);
+
+ // Ensure rootNode is not null
+ if (rootNode == null) {
+ rootNode = new JSONObject(); // Initialize rootNode if it's null
+ }
+
+ // Ensure loyalty_scores array exists
+ JSONArray loyaltyScoresArray = rootNode.optJSONArray("loyalty_scores");
+ if (loyaltyScoresArray == null) {
+ loyaltyScoresArray = new JSONArray(); // Create a new empty array if it's null
+ rootNode.put("loyalty_scores", loyaltyScoresArray); // Add the array to rootNode
+ }
+
+ // Create new loyalty score entry
+ JSONObject newLoyaltyScore = new JSONObject();
+ newLoyaltyScore.put("date", date);
+ newLoyaltyScore.put("artist_name", artist_name);
+ newLoyaltyScore.put("score", score);
+
+ // Add the new loyalty score to the array
+ loyaltyScoresArray.put(newLoyaltyScore);
+
+ // Write the updated data back to the file
+ writeUserFile(userid, rootNode, loyaltyScoresArray);
+ }
+
+ @Override
+ public Map getLoyaltyArtist(String userid, String artist_name) {
+ Map loyaltyScores = new HashMap<>();
+ JSONObject rootNode = readUserFile(userid);
+ if (rootNode == null) {
+ return loyaltyScores; // Early return if the file doesn't exist
+ }
+
+ JSONArray loyaltyScoresArray = rootNode.optJSONArray("loyalty_scores");
+ if (loyaltyScoresArray == null) {
+ return loyaltyScores; // Early return if the loyalty_scores array doesn't exist
+ }
+
+ loyaltyScoresArray.forEach(entry -> {
+ JSONObject loyaltyScore = (JSONObject) entry;
+ if (loyaltyScore.getString("artist_name").equals(artist_name)) {
+ String date = loyaltyScore.getString("date");
+ int score = loyaltyScore.getInt("score");
+ loyaltyScores.put(date, score);
+ }
+ });
+ return loyaltyScores;
+ }
+
+ // Helper method to get the file for a specific user
+ private File getUserFile(String userid) {
+ return new File("loyaltyscore_" + userid + ".json");
+ }
+
+ // Helper method to read the user's file and return a JSONObject
+ private JSONObject readUserFile(String userid) {
+ File userFile = getUserFile(userid);
+ if (!userFile.exists()) {
+ return null; // Return null if the file doesn't exist
+ }
+
+ try (BufferedReader br = new BufferedReader(new FileReader(userFile))) {
+ StringBuilder jsonContent = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ jsonContent.append(line);
+ }
+
+ // If the file is empty, return a new JSONObject with an empty "loyalty_scores" array
+ if (jsonContent.length() == 0) {
+ JSONObject emptyRoot = new JSONObject();
+ emptyRoot.put("loyalty_scores", new JSONArray());
+ return emptyRoot;
+ }
+
+ return new JSONObject(jsonContent.toString());
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null; // Return null in case of an error reading the file
+ }
+ }
+
+ // Helper method to write the JSONObject back to the user's file
+ private void writeUserFile(String userid, JSONObject rootNode, JSONArray loyaltyScoresArray) {
+ rootNode.put("loyalty_scores", loyaltyScoresArray);
+ try (FileWriter fileWriter = new FileWriter(getUserFile(userid))) {
+ fileWriter.write(rootNode.toString(4)); // Pretty print with indentation level 4
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/data_access/SpotifyDataAccessObject.java b/src/main/java/data_access/SpotifyDataAccessObject.java
new file mode 100644
index 00000000..f4a28bd9
--- /dev/null
+++ b/src/main/java/data_access/SpotifyDataAccessObject.java
@@ -0,0 +1,363 @@
+package data_access;
+
+import entity.ArtistLoyaltyScore;
+import entity.SpotifyUser;
+import okhttp3.*;
+import org.json.JSONObject;
+import se.michaelthelin.spotify.SpotifyApi;
+import se.michaelthelin.spotify.model_objects.specification.*;
+import util.ConfigManager;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.*;
+
+public class SpotifyDataAccessObject {
+ private final SpotifyApi spotifyApi;
+ private final String clientId;
+ private final String redirectUri;
+ private String codeVerifier;
+ private final OkHttpClient httpClient;
+
+ public SpotifyDataAccessObject() {
+ this.clientId = ConfigManager.getProperty("spotify.client.id");
+ this.redirectUri = ConfigManager.getProperty("spotify.redirect.uri");
+ this.httpClient = new OkHttpClient();
+
+ if ("default_client_id".equals(this.clientId)) {
+ System.err.println("ERROR: Please set SPOTIFY_CLIENT_ID environment variable");
+ System.err.println("Get it from: https://developer.spotify.com/dashboard");
+ }
+
+ this.spotifyApi = new SpotifyApi.Builder()
+ .setClientId(this.clientId)
+ .setRedirectUri(URI.create(this.redirectUri))
+ .build();
+ }
+
+ private String generateCodeVerifier() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] codeVerifierBytes = new byte[32];
+ secureRandom.nextBytes(codeVerifierBytes);
+ return Base64.getUrlEncoder().withoutPadding()
+ .encodeToString(codeVerifierBytes);
+ }
+
+ private String generateCodeChallenge(String codeVerifier) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
+ return Base64.getUrlEncoder().withoutPadding()
+ .encodeToString(hash);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to generate code challenge", e);
+ }
+ }
+
+ public String getAuthorizationUrl() {
+ try {
+ this.codeVerifier = generateCodeVerifier();
+ String codeChallenge = generateCodeChallenge(codeVerifier);
+
+ System.out.println("Generated code verifier (length: " + codeVerifier.length() + ")");
+ System.out.println("Generated code challenge: " + codeChallenge);
+
+ // Build authorization URL manually with PKCE parameters
+ String scope = "user-library-read user-read-recently-played user-top-read";
+ String authUrl = String.format(
+ "https://accounts.spotify.com/authorize?" +
+ "client_id=%s&" +
+ "response_type=code&" +
+ "redirect_uri=%s&" +
+ "code_challenge_method=S256&" +
+ "code_challenge=%s&" +
+ "scope=%s",
+ URLEncoder.encode(clientId, StandardCharsets.UTF_8),
+ URLEncoder.encode(redirectUri, StandardCharsets.UTF_8),
+ URLEncoder.encode(codeChallenge, StandardCharsets.UTF_8),
+ URLEncoder.encode(scope, StandardCharsets.UTF_8)
+ );
+
+ System.out.println("Authorization URL: " + authUrl);
+ return authUrl;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public SpotifyUser exchangeCodeForTokens(String authorizationCode, String username) {
+ try {
+ if (codeVerifier == null) {
+ throw new RuntimeException("Code verifier not generated. Call getAuthorizationUrl() first.");
+ }
+
+ System.out.println("Exchanging code for tokens with PKCE...");
+ System.out.println("Authorization code: " + authorizationCode);
+ System.out.println("Using code verifier");
+
+ // Exchange code for tokens using PKCE via HTTP request
+ RequestBody formBody = new FormBody.Builder()
+ .add("grant_type", "authorization_code")
+ .add("code", authorizationCode)
+ .add("redirect_uri", redirectUri)
+ .add("client_id", clientId)
+ .add("code_verifier", codeVerifier)
+ .build();
+
+ Request request = new Request.Builder()
+ .url("https://accounts.spotify.com/api/token")
+ .post(formBody)
+ .addHeader("Content-Type", "application/x-www-form-urlencoded")
+ .build();
+
+ try (Response response = httpClient.newCall(request).execute()) {
+ String responseBody = response.body().string();
+
+ if (!response.isSuccessful()) {
+ System.err.println("Token exchange failed: " + responseBody);
+ throw new RuntimeException("Token exchange failed: " + responseBody);
+ }
+
+ JSONObject json = new JSONObject(responseBody);
+ String accessToken = json.getString("access_token");
+ String refreshToken = json.optString("refresh_token", null);
+
+ System.out.println("Successfully obtained access token");
+
+ spotifyApi.setAccessToken(accessToken);
+ if (refreshToken != null) {
+ spotifyApi.setRefreshToken(refreshToken);
+ }
+
+ // Get Spotify user profile
+ var userProfile = spotifyApi.getCurrentUsersProfile().build().execute();
+ System.out.println("Got user profile: " + userProfile.getDisplayName());
+
+ SpotifyUser spotifyUser = new SpotifyUser(
+ username,
+ accessToken,
+ refreshToken,
+ userProfile.getId()
+ );
+
+ // Clear code verifier after use
+ this.codeVerifier = null;
+
+ return spotifyUser;
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to exchange authorization code: " + e.getMessage(), e);
+ }
+ }
+
+ public SpotifyApi getSpotifyApiForUser(SpotifyUser user) {
+ return new SpotifyApi.Builder()
+ .setAccessToken(user.getAccessToken())
+ .setRefreshToken(user.getRefreshToken())
+ .setClientId(clientId)
+ .build();
+ }
+
+ public SpotifyUser refreshAccessToken(SpotifyUser user) {
+ try {
+ if (user.getRefreshToken() == null) {
+ throw new RuntimeException("No refresh token available");
+ }
+
+ System.out.println("Refreshing access token...");
+
+ RequestBody formBody = new FormBody.Builder()
+ .add("grant_type", "refresh_token")
+ .add("refresh_token", user.getRefreshToken())
+ .add("client_id", clientId)
+ .build();
+
+ Request request = new Request.Builder()
+ .url("https://accounts.spotify.com/api/token")
+ .post(formBody)
+ .addHeader("Content-Type", "application/x-www-form-urlencoded")
+ .build();
+
+ try (Response response = httpClient.newCall(request).execute()) {
+ String responseBody = response.body().string();
+
+ if (!response.isSuccessful()) {
+ System.err.println("Token refresh failed: " + responseBody);
+ throw new RuntimeException("Token refresh failed: " + responseBody);
+ }
+
+ JSONObject json = new JSONObject(responseBody);
+ String accessToken = json.getString("access_token");
+ String refreshToken = json.optString("refresh_token", user.getRefreshToken());
+
+ System.out.println("Successfully refreshed access token");
+
+ return new SpotifyUser(
+ user.getUsername(),
+ accessToken,
+ refreshToken,
+ user.getSpotifyUserId()
+ );
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to refresh access token: " + e.getMessage(), e);
+ }
+ }
+
+ public List getArtistLoyaltyScores(SpotifyUser user) {
+ try {
+ SpotifyApi userApi = getSpotifyApiForUser(user);
+
+ System.out.println("Fetching saved tracks...");
+ // Get saved tracks
+ Map artistTrackCount = new HashMap<>();
+ Map artistNames = new HashMap<>();
+
+ Paging savedTracks = userApi.getUsersSavedTracks().limit(50).build().execute();
+ System.out.println("Got " + savedTracks.getItems().length + " saved tracks");
+
+ for (SavedTrack savedTrack : savedTracks.getItems()) {
+ Track track = savedTrack.getTrack();
+ for (ArtistSimplified artist : track.getArtists()) {
+ String artistId = artist.getId();
+ artistTrackCount.put(artistId, artistTrackCount.getOrDefault(artistId, 0) + 1);
+ artistNames.put(artistId, artist.getName());
+ }
+ }
+
+ System.out.println("Fetching saved albums...");
+ // Get saved albums
+ Map artistAlbumCount = new HashMap<>();
+ Paging savedAlbums = userApi.getCurrentUsersSavedAlbums().limit(50).build().execute();
+ System.out.println("Got " + savedAlbums.getItems().length + " saved albums");
+
+ for (SavedAlbum savedAlbum : savedAlbums.getItems()) {
+ Album album = savedAlbum.getAlbum();
+ for (ArtistSimplified artist : album.getArtists()) {
+ String artistId = artist.getId();
+ artistAlbumCount.put(artistId, artistAlbumCount.getOrDefault(artistId, 0) + 1);
+ artistNames.putIfAbsent(artistId, artist.getName());
+ }
+ }
+
+ System.out.println("Fetching recently played tracks...");
+ // Get recently played tracks
+ Set recentlyPlayedArtists = new HashSet<>();
+ PagingCursorbased recentlyPlayed = userApi.getCurrentUsersRecentlyPlayedTracks()
+ .limit(50).build().execute();
+ System.out.println("Got " + recentlyPlayed.getItems().length + " recently played tracks");
+
+ for (PlayHistory playHistory : recentlyPlayed.getItems()) {
+ Track track = playHistory.getTrack();
+ for (ArtistSimplified artist : track.getArtists()) {
+ recentlyPlayedArtists.add(artist.getId());
+ artistNames.putIfAbsent(artist.getId(), artist.getName());
+ }
+ }
+
+ // Combine all artists
+ Set allArtists = new HashSet<>();
+ allArtists.addAll(artistTrackCount.keySet());
+ allArtists.addAll(artistAlbumCount.keySet());
+ allArtists.addAll(recentlyPlayedArtists);
+
+ System.out.println("Total unique artists: " + allArtists.size());
+
+ // Create loyalty scores
+ List loyaltyScores = new ArrayList<>();
+ for (String artistId : allArtists) {
+ String artistName = artistNames.get(artistId);
+ int trackCount = artistTrackCount.getOrDefault(artistId, 0);
+ int albumCount = artistAlbumCount.getOrDefault(artistId, 0);
+ boolean inRecent = recentlyPlayedArtists.contains(artistId);
+
+ ArtistLoyaltyScore score = new ArtistLoyaltyScore(
+ artistName, artistId, trackCount, albumCount, inRecent
+ );
+ loyaltyScores.add(score);
+ }
+
+ // Sort by loyalty score descending
+ loyaltyScores.sort((a, b) -> Double.compare(b.getLoyaltyScore(), a.getLoyaltyScore()));
+
+ System.out.println("Calculated loyalty scores for " + loyaltyScores.size() + " artists");
+
+ return loyaltyScores;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to get artist loyalty scores: " + e.getMessage(), e);
+ }
+ }
+
+ // NEW: Generate a Daily Mix of tracks based on user's saved and recently played tracks
+ public List generateDailyMix(SpotifyUser user, int mixSize) {
+ try {
+ SpotifyApi userApi = getSpotifyApiForUser(user);
+
+ List