Changeset db5fb23


Ignore:
Timestamp:
02/12/26 11:27:23 (5 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Children:
d85e8e3
Parents:
92db381
Message:

add endpoint for publishing songs and albums

Files:
4 added
10 edited

Legend:

Unmodified
Added
Removed
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/MusicalEntityController.java

    r92db381 rdb5fb23  
    22
    33import com.ukim.finki.develop.finkwave.model.dto.LikeStatusDto;
     4import com.ukim.finki.develop.finkwave.model.dto.PublishAlbumRequestDto;
     5import com.ukim.finki.develop.finkwave.model.dto.PublishSongRequestDto;
    46import com.ukim.finki.develop.finkwave.service.LikeService;
     7import com.ukim.finki.develop.finkwave.service.MusicalEntityService;
    58import lombok.AllArgsConstructor;
    69import org.springframework.http.HttpEntity;
     10import org.springframework.http.HttpStatus;
     11import org.springframework.http.MediaType;
    712import org.springframework.http.ResponseEntity;
    8 import org.springframework.web.bind.annotation.PathVariable;
    9 import org.springframework.web.bind.annotation.PostMapping;
    10 import org.springframework.web.bind.annotation.RequestMapping;
    11 import org.springframework.web.bind.annotation.RestController;
     13import org.springframework.web.bind.annotation.*;
     14
     15import java.io.IOException;
    1216
    1317@RestController
     
    1721
    1822    private final LikeService likeService;
     23    private final MusicalEntityService musicalEntityService;
    1924
    2025    @PostMapping("/{id}/like")
     
    2227        return ResponseEntity.ok(likeService.toggleLike(entityId));
    2328    }
     29
     30    @PostMapping(value = "/publish/song", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     31    public HttpEntity<?> publishSong(@ModelAttribute PublishSongRequestDto publishSongRequestDto) throws IOException {
     32        musicalEntityService.handleSongPublish(publishSongRequestDto);
     33        return ResponseEntity.status(HttpStatus.CREATED).build();
     34    }
     35
     36    @PostMapping(value = "/publish/album", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     37    public HttpEntity<?> publishAlbum(@ModelAttribute PublishAlbumRequestDto publishAlbumRequestDto) throws IOException {
     38        musicalEntityService.handleAlbumPublish(publishAlbumRequestDto);
     39        return ResponseEntity.status(HttpStatus.CREATED).build();
     40    }
    2441}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/MusicalEntity.java

    r92db381 rdb5fb23  
    33import java.time.LocalDate;
    44
     5import jakarta.persistence.*;
     6import lombok.*;
    57import org.hibernate.annotations.OnDelete;
    68import org.hibernate.annotations.OnDeleteAction;
    79
    810import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    9 
    10 import jakarta.persistence.Column;
    11 import jakarta.persistence.Entity;
    12 import jakarta.persistence.FetchType;
    13 import jakarta.persistence.Id;
    14 import jakarta.persistence.JoinColumn;
    15 import jakarta.persistence.ManyToOne;
    16 import jakarta.persistence.Table;
    17 import lombok.Getter;
    18 import lombok.Setter;
    1911
    2012@Getter
     
    2315@Table(name = "musical_entities", schema = "project")
    2416@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
     17@Builder
     18@NoArgsConstructor
     19@AllArgsConstructor
    2520public class MusicalEntity {
    2621    @Id
    2722    @Column(name = "id", nullable = false)
     23    @GeneratedValue(strategy = GenerationType.IDENTITY)
    2824    private Long id;
    2925
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/Song.java

    r92db381 rdb5fb23  
    1515    @Id
    1616    @Column(name = "id", nullable = false)
     17    @GeneratedValue(strategy = GenerationType.IDENTITY)
    1718    private Long id;
    1819
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/ArtistContributionSummaryDto.java

    r92db381 rdb5fb23  
    33import lombok.AllArgsConstructor;
    44import lombok.Getter;
     5import lombok.NoArgsConstructor;
     6import lombok.Setter;
    57
    68// this dto is useful when we already have all the necessary info for the song and just need the contribution info
    79@Getter
     10@Setter
     11@NoArgsConstructor
    812@AllArgsConstructor
    913public class ArtistContributionSummaryDto {
     14    Long id = null;
    1015    private String artistName;
    1116    private String role;
     17
     18    public ArtistContributionSummaryDto(String artistName, String role) {
     19        this.artistName = artistName;
     20        this.role = role;
     21    }
    1222}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/UserSearchResultDto.java

    r92db381 rdb5fb23  
    33
    44public record UserSearchResultDto(
    5     String fullName,
    6     String username,
    7     String profilePhoto
     5        Long id,
     6        String fullName,
     7        String username,
     8        String profilePhoto
    89){}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/UserRepository.java

    r92db381 rdb5fb23  
    1717
    1818    @Query(value = """
    19         SELECT u.full_name, u.username, u.profile_photo from users u
     19        SELECT u.user_id, u.full_name, u.username, u.profile_photo from users u
    2020        WHERE (u.full_name ILIKE '%' || :searchTerm || '%' or u.username ILIKE '%' || :searchTerm || '%')
    2121            and u.listener = true and u.artist = false
     
    2525
    2626    @Query(value = """
    27         SELECT u.full_name, u.username, u.profile_photo from users u
     27        SELECT u.user_id, u.full_name, u.username, u.profile_photo from users u
    2828        WHERE (u.full_name ILIKE '%' || :searchTerm || '%' or u.username ILIKE '%' || :searchTerm || '%') and u.artist = true
    2929        LIMIT :limit
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/ArtistService.java

    r92db381 rdb5fb23  
    11package com.ukim.finki.develop.finkwave.service;
    22
     3import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException;
     4import com.ukim.finki.develop.finkwave.model.Artist;
    35import com.ukim.finki.develop.finkwave.model.dto.ArtistContributionDto;
    46import com.ukim.finki.develop.finkwave.repository.ArtistContributionRepository;
    57
     8import com.ukim.finki.develop.finkwave.repository.ArtistRepository;
    69import org.springframework.transaction.annotation.Transactional;
    710import lombok.AllArgsConstructor;
     
    1417public class ArtistService {
    1518    private final ArtistContributionRepository artistContributionRepository;
     19    private final ArtistRepository artistRepository;
    1620
    1721
     
    2125    }
    2226
     27    public Artist getArtistById(Long id){
     28        return artistRepository.findById(id).orElseThrow(UserNotFoundException::new);
     29    }
     30
    2331}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AuthService.java

    r92db381 rdb5fb23  
    134134    }
    135135
    136     private User createNonAdminUser(AuthRequestDto authRequestDto) throws IOException {
     136    @Transactional
     137    public User createNonAdminUser(AuthRequestDto authRequestDto) throws IOException {
    137138        User.UserBuilder userBuilder = User.builder()
    138139                .username(authRequestDto.username())
  • frontend/src/pages/PublishSong.tsx

    r92db381 rdb5fb23  
    2323        const [genre, setGenre] = useState("");
    2424        const [link, setLink] = useState("");
    25         const [_coverFile, setCoverFile] = useState<File | null>(null); // TODO: Use in API call
     25        const [coverFile, setCoverFile] = useState<File | null>(null);
    2626        const [coverPreview, setCoverPreview] = useState<string | null>(null);
    2727        const [contributors, setContributors] = useState<Contributor[]>([]);
     
    9393                                `/users/search?type=ARTIST&q=${encodeURIComponent(query)}&limit=5`,
    9494                        );
    95 
    9695                        const filtered = response.data.filter(
    9796                                (artist: ArtistSearchResult) =>
     
    129128                }
    130129
    131                 if (contributors.some((c) => c.username === artist.username)) {
     130                if (contributors.some((c) => c.id === artist.id)) {
    132131                        toast.error("This contributor is already added");
    133132                        return;
     
    137136                        ...contributors,
    138137                        {
    139                                 username: artist.username,
     138                                id: artist.id,
    140139                                fullName: artist.fullName,
    141140                                role: selectedContributorRole,
     
    148147
    149148        // Remove contributor
    150         const handleRemoveContributor = (username: string) => {
    151                 setContributors(contributors.filter((c) => c.username !== username));
     149        const handleRemoveContributor = (id: number) => {
     150                setContributors(contributors.filter((c) => c.id !== id));
    152151        };
    153152
     
    242241                }
    243242
    244                 if (song.contributors.some((c) => c.username === artist.username)) {
     243                if (song.contributors.some((c) => c.id === artist.id)) {
    245244                        toast.error("This contributor is already added");
    246245                        return;
     
    249248                const role = albumSongSelectedRole[songIndex] || ROLE_OPTIONS[0].value;
    250249                const newContributor: Contributor = {
    251                         username: artist.username,
     250                        id: artist.id,
    252251                        fullName: artist.fullName,
    253252                        role,
     
    263262        };
    264263
    265         const handleRemoveAlbumSongContributor = (
    266                 songIndex: number,
    267                 username: string,
    268         ) => {
     264        const handleRemoveAlbumSongContributor = (songIndex: number, id: number) => {
    269265                const song = albumSongs[songIndex];
    270266                handleAlbumSongChange(
    271267                        songIndex,
    272268                        "contributors",
    273                         song.contributors.filter((c) => c.username !== username),
     269                        song.contributors.filter((c) => c.id !== id),
    274270                );
    275271        };
    276272
    277         // Form submission
    278273        const handleSubmit = async (e: React.FormEvent) => {
    279274                e.preventDefault();
    280275
    281                 // Validation
    282276                if (!title.trim()) {
    283277                        toast.error("Please enter a title");
     
    295289                        }
    296290                } else {
    297                         // Album validation
    298291                        for (let i = 0; i < albumSongs.length; i++) {
    299292                                if (!albumSongs[i].title.trim()) {
     
    311304
    312305                try {
    313                         // TODO: replace with API call
    314                         // const formData = new FormData();
    315                         // formData.append('title', title);
    316                         // formData.append('genre', genre);
    317                         // if (coverFile) formData.append('cover', coverFile);
    318                         // ...
    319 
    320                         // Simulate API call
    321                         await new Promise((resolve) => setTimeout(resolve, 1000));
     306                        const formData = new FormData();
     307                        formData.append("title", title);
     308                        formData.append("genre", genre);
     309                        if (coverFile) formData.append("cover", coverFile);
     310
     311                        // Helper to append contributors in indexed format for Spring @ModelAttribute
     312                        const appendContributors = (
     313                                formData: FormData,
     314                                contributors: Contributor[],
     315                                prefix: string,
     316                        ) => {
     317                                contributors.forEach((c, i) => {
     318                                        formData.append(`${prefix}[${i}].id`, c.id.toString());
     319                                        formData.append(`${prefix}[${i}].artistName`, c.fullName);
     320                                        formData.append(`${prefix}[${i}].role`, c.role);
     321                                });
     322                        };
     323
     324                        if (releaseType === "album") {
     325                                // Append album songs in indexed format
     326                                albumSongs.forEach((song, songIndex) => {
     327                                        formData.append(`albumSongs[${songIndex}].title`, song.title);
     328                                        formData.append(`albumSongs[${songIndex}].link`, song.link);
     329                                        // Append nested contributors for each song
     330                                        song.contributors.forEach((c, contribIndex) => {
     331                                                formData.append(
     332                                                        `albumSongs[${songIndex}].contributors[${contribIndex}].id`,
     333                                                        c.id.toString(),
     334                                                );
     335                                                formData.append(
     336                                                        `albumSongs[${songIndex}].contributors[${contribIndex}].artistName`,
     337                                                        c.fullName,
     338                                                );
     339                                                formData.append(
     340                                                        `albumSongs[${songIndex}].contributors[${contribIndex}].role`,
     341                                                        c.role,
     342                                                );
     343                                        });
     344                                });
     345                                await axiosInstance.post("/musical-entity/publish/album", formData, {
     346                                        headers: { "Content-Type": "multipart/form-data" },
     347                                });
     348                        } else {
     349                                formData.append("link", link);
     350                                appendContributors(formData, contributors, "contributors");
     351                                await axiosInstance.post("/musical-entity/publish/song", formData, {
     352                                        headers: { "Content-Type": "multipart/form-data" },
     353                                });
     354                        }
    322355
    323356                        toast.success(
     
    612645                                                                        {contributors.map((contributor) => (
    613646                                                                                <div
    614                                                                                         key={contributor.username}
     647                                                                                        key={contributor.id}
    615648                                                                                        className="flex items-center gap-2 bg-[#282828] rounded-full py-1.5 px-3"
    616649                                                                                >
     
    626659                                                                                        <button
    627660                                                                                                type="button"
    628                                                                                                 onClick={() =>
    629                                                                                                         handleRemoveContributor(contributor.username)
    630                                                                                                 }
     661                                                                                                onClick={() => handleRemoveContributor(contributor.id)}
    631662                                                                                                className="text-gray-400 hover:text-red-400 transition-colors"
    632663                                                                                        >
     
    687718                                                                                                        searchResults.map((artist) => (
    688719                                                                                                                <button
    689                                                                                                                         key={artist.username}
     720                                                                                                                        key={artist.id}
    690721                                                                                                                        type="button"
    691722                                                                                                                        onClick={() => handleAddContributor(artist)}
     
    808839                                                                                                {song.contributors.map((contributor) => (
    809840                                                                                                        <div
    810                                                                                                                 key={contributor.username}
     841                                                                                                                key={contributor.id}
    811842                                                                                                                className="flex items-center gap-2 bg-[#1a1a2e] rounded-full py-1 px-2.5 text-sm"
    812843                                                                                                        >
     
    826857                                                                                                                                handleRemoveAlbumSongContributor(
    827858                                                                                                                                        index,
    828                                                                                                                                         contributor.username,
     859                                                                                                                                        contributor.id,
    829860                                                                                                                                )
    830861                                                                                                                        }
     
    902933                                                                                                                                                (artist) => (
    903934                                                                                                                                                        <button
    904                                                                                                                                                                 key={artist.username}
     935                                                                                                                                                                key={artist.id}
    905936                                                                                                                                                                type="button"
    906937                                                                                                                                                                onClick={() =>
  • frontend/src/utils/types.ts

    r92db381 rdb5fb23  
    121121
    122122export interface Contributor {
    123         username: string;
     123        id: number;
    124124        fullName: string;
    125125        role: string;
     
    127127
    128128export interface ArtistSearchResult {
     129        id: number;
    129130        username: string;
    130131        fullName: string;
Note: See TracChangeset for help on using the changeset viewer.