Skip to content

Commit

Permalink
Merge pull request #46 from PartyHall/feature/filters
Browse files Browse the repository at this point in the history
feature(karaoke): Adding filters to search
  • Loading branch information
oxodao authored Dec 28, 2024
2 parents 92916f8 + 1d35aa8 commit 125075d
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 36 deletions.
12 changes: 11 additions & 1 deletion admin/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@
"title": "Failed to add/remove song in the queue",
"description": "See console for more infos"
},
"singer": "Singer"
"singer": "Singer",
"filter": {
"title": "Filters",
"format": "Format",
"video": "Video",
"cdg": "CDG",
"transparent_video": "Transparent video",
"has_vocals": "Has vocals",
"yes": "Yes",
"no": "No"
}
}
}
12 changes: 11 additions & 1 deletion admin/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@
"title": "Échec de l'ajout/suppression de la musique",
"description": "Voir la console pour plus d'infos"
},
"singer": "Chanteur"
"singer": "Chanteur",
"filter": {
"title": "Filtres",
"format": "Format",
"video": "Vidéo",
"cdg": "CDG",
"transparent_video": "Vidéo transparente",
"has_vocals": "Voix",
"yes": "Oui",
"no": "Non"
}
}
}
64 changes: 59 additions & 5 deletions admin/src/components/karaoke/song_search.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,97 @@
import { Button, Checkbox, Flex, Input, Pagination, Popover, Segmented, Typography } from 'antd';
import { Collection, PhSong } from '@partyhall/sdk';
import { Flex, Input, Pagination, Typography } from 'antd';

import { IconFilter } from '@tabler/icons-react';
import Loader from '../loader';
import SongCard from './song_card';

import useAsyncEffect from 'use-async-effect';
import { useAuth } from '../../hooks/auth';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CheckboxChangeEvent } from 'antd/es/checkbox';

export default function SongSearch() {
const { t } = useTranslation('', { keyPrefix: 'karaoke' });
const { t: tG } = useTranslation('', { keyPrefix: 'generic' });
const { api } = useAuth();

const [search, setSearch] = useState<string>('');
const [formats, setFormats] = useState<string[]>([]);
const [hasVocals, setHasVocals] = useState<boolean | null>(null);

const [loading, setLoading] = useState<boolean>(true);
const [page, setPage] = useState<number>(1);
const [songs, setSongs] = useState<Collection<PhSong> | null>(null);

useAsyncEffect(async () => {
setLoading(true);
setSongs(await api.karaoke.getCollection(page, search));
setSongs(await api.karaoke.getCollection(page, search, formats, hasVocals));
setLoading(false);
}, [page, search]);
}, [page, search, formats, hasVocals]);

const onCheckboxChange = (e: CheckboxChangeEvent) => {
if (!e.target.checked) {
setFormats(formats.filter((y) => y !== e.target.value));
setPage(1);

return;
}

if (formats.includes(e.target.value)) {
return;
}

setFormats([...formats, e.target.value]);
setPage(1);
};

return (
<>
<Flex align="center" justify="center">
<Flex align="center" justify="center" gap={8} style={{ width: 'min(100%, 500px)', margin: 'auto' }}>
<Input
style={{ width: 'min(100%, 500px)' }}
placeholder={tG('actions.search') + '...'}
value={search}
onChange={(x) => {
setSearch(x.target.value);
setPage(1);
}}
/>
<Popover
title={t('filter.title')}
trigger="click"
content={
<Flex vertical gap={8}>
<Typography.Text>{t('filter.has_vocals')}:</Typography.Text>
<Segmented
block
options={[
{ value: false, label: t('filter.no') },
{ value: null, label: '-' },
{ value: true, label: t('filter.yes') },
]}
value={hasVocals}
onChange={(x) => setHasVocals(x)}
/>
<Typography.Text>{t('filter.format')}:</Typography.Text>
<Checkbox value="video" checked={formats.includes('video')} onChange={onCheckboxChange}>
{t('filter.video')}
</Checkbox>
<Checkbox value="cdg" checked={formats.includes('cdg')} onChange={onCheckboxChange}>
{t('filter.cdg')}
</Checkbox>
<Checkbox
value="transparent_video"
checked={formats.includes('transparent_video')}
onChange={onCheckboxChange}
>
{t('filter.transparent_video')}
</Checkbox>
</Flex>
}
>
<Button icon={<IconFilter size={20} />} />
</Popover>
</Flex>
<Loader loading={loading}>
{(!songs || songs.totalCount === 0) && (
Expand Down
5 changes: 5 additions & 0 deletions admin/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ const phTheme = {
Modal: {
contentBg: 'rgb(23,21,32)',
},
Segmented: {
itemActiveBg: '#f92aa9',
itemSelectedBg: '#f92aa9',
itemSelectedColor: 'rgb(23,21,32)',
},
},
algorithm: [theme.darkAlgorithm, theme.compactAlgorithm],
};
Expand Down
5 changes: 1 addition & 4 deletions ansible/reboot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@

tasks:
- name: 'Reboot to finish the setup'
reboot:
reboot_timeout: 1
connect_timeout: 1
shell: "sleep 2 && reboot &"
async: 1
poll: 0
failed_when: false
ignore_errors: true
71 changes: 54 additions & 17 deletions backend/dal/song.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/partyhall/partyhall/log"
"github.com/partyhall/partyhall/models"
"github.com/partyhall/partyhall/utils"
)

var SONGS Songs
Expand Down Expand Up @@ -48,20 +49,51 @@ func (s Songs) GetAll() ([]models.Song, error) {
return songs, nil
}

func (s Songs) GetCollection(search string, amt, offset int) (*models.PaginatedResponse, error) {
func (s Songs) GetCollection(
search string,
formats []string,
hasVocals utils.Thrilean,
amt,
offset int,
) (*models.PaginatedResponse, error) {
args := []any{search, search}

formatClause := ""
if len(formats) > 0 {
formatPlaceholders := make([]string, len(formats))
for i := range formats {
formatPlaceholders[i] = "?"
}

formatClause = fmt.Sprintf("AND s.format IN (%s)", strings.Join(formatPlaceholders, ", "))

for _, format := range formats {
args = append(args, format)
}
}

vocalsClause := ""
if !hasVocals.IsNull {
vocalsClause = "AND s.has_vocals = ?"
args = append(args, hasVocals.Value)
}

resp := models.PaginatedResponse{}

row := DB.QueryRow(`
row := DB.QueryRow(fmt.Sprintf(`
SELECT COUNT(DISTINCT s.rowid)
FROM song s
JOIN songs_fts fts ON s.rowid = fts.rowid
WHERE LENGTH(?) = 0
OR s.rowid IN (
SELECT rowid
FROM songs_fts
WHERE songs_fts MATCH ? || '*'
)
`, search, search)
WHERE (
LENGTH(?) = 0
OR s.rowid IN (
SELECT rowid
FROM songs_fts
WHERE songs_fts MATCH ? || '*'
)
)
%s %s
`, formatClause, vocalsClause), args...)
if row.Err() != nil {
return nil, row.Err()
}
Expand All @@ -73,7 +105,9 @@ func (s Songs) GetCollection(search string, amt, offset int) (*models.PaginatedR

resp.CalculateMaxPage()

rows, err := DB.Queryx(`
args = append(args, amt, offset)

rows, err := DB.Queryx(fmt.Sprintf(`
SELECT
s.nexus_id,
s.title,
Expand All @@ -87,15 +121,18 @@ func (s Songs) GetCollection(search string, amt, offset int) (*models.PaginatedR
s.has_combined
FROM song s
JOIN songs_fts fts ON s.rowid = fts.rowid
WHERE LENGTH(?) = 0
OR s.rowid IN (
SELECT rowid
FROM songs_fts
WHERE songs_fts MATCH ? || '*'
)
WHERE (
LENGTH(?) = 0
OR s.rowid IN (
SELECT rowid
FROM songs_fts
WHERE songs_fts MATCH ? || '*'
)
)
%s %s
ORDER BY s.artist ASC, s.title ASC
LIMIT ? OFFSET ?
`, search, search, amt, offset)
`, formatClause, vocalsClause), args...)

if err != nil {
return nil, err
Expand Down
45 changes: 38 additions & 7 deletions backend/routes/songs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,21 @@ import (
routes_requests "github.com/partyhall/partyhall/routes/requests"
"github.com/partyhall/partyhall/services"
"github.com/partyhall/partyhall/state"
"github.com/partyhall/partyhall/utils"
)

func routeGetSongs(c *gin.Context) {
offset := 0

search := c.Query("search")
page := c.Query("page")
pageStr := c.Query("page")
formatsStr := c.Query("formats")
hasVocalsStr := c.Query("has_vocals")

var pageInt int = 1
var page int = 1
var err error
if len(page) > 0 {
pageInt, err = strconv.Atoi(page)
if len(pageStr) > 0 {
page, err = strconv.Atoi(pageStr)
if err != nil {
c.Render(http.StatusBadRequest, api_errors.INVALID_PARAMETERS.WithExtra(map[string]any{
"page": "The page should be an integer",
Expand All @@ -42,10 +45,38 @@ func routeGetSongs(c *gin.Context) {
return
}

offset = (pageInt - 1) * config.AMT_RESULTS_PER_PAGE
offset = (page - 1) * config.AMT_RESULTS_PER_PAGE
}

songs, err := dal.SONGS.GetCollection(search, config.AMT_RESULTS_PER_PAGE, offset)
formats := []string{}
if len(formatsStr) > 0 {
formats = strings.Split(strings.ToLower(formatsStr), ",")
}

hasVocals := utils.Thrilean{IsNull: true}
if len(hasVocalsStr) > 0 {
hasVocalsBool, err := strconv.ParseBool(hasVocalsStr)
if err != nil {
c.Render(http.StatusBadRequest, api_errors.INVALID_PARAMETERS.WithExtra(map[string]any{
"has_vocals": "Should be either left blank, true or false",
}))

return
}

hasVocals = utils.Thrilean{
Value: hasVocalsBool,
IsNull: false,
}
}

songs, err := dal.SONGS.GetCollection(
search,
formats,
hasVocals,
config.AMT_RESULTS_PER_PAGE,
offset,
)
if err != nil {
c.Render(http.StatusInternalServerError, api_errors.DATABASE_ERROR.WithExtra(map[string]any{
"err": err.Error(),
Expand All @@ -54,7 +85,7 @@ func routeGetSongs(c *gin.Context) {
return
}

songs.Page = pageInt
songs.Page = page

c.JSON(http.StatusOK, songs)
}
Expand Down
6 changes: 6 additions & 0 deletions backend/utils/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package utils

type Thrilean struct {
Value bool
IsNull bool
}
15 changes: 14 additions & 1 deletion sdk/src/karaoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ export default class Karaoke {
this.sdk = sdk;
}

public async getCollection(page: number | null, search?: string | null): Promise<Collection<PhSong>> {
public async getCollection(
page: number | null,
search?: string | null,
formats: string[] = [],
hasVocals: boolean | null = null
): Promise<Collection<PhSong>> {
if (!page) {
page = 1;
}
Expand All @@ -18,6 +23,14 @@ export default class Karaoke {
query.set('search', search);
}

if (formats.length > 0) {
query.set('formats', formats.join(','));
}

if (hasVocals !== null) {
query.set('has_vocals', hasVocals ? 'true' : 'false');
}

const resp = await this.sdk.get(`/api/webapp/songs?${query.toString()}`);
const data = await resp.json();

Expand Down

0 comments on commit 125075d

Please sign in to comment.