Files
BYW-VoiceHub/app/composables/useSongs.ts
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

969 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { computed, ref } from 'vue'
import { useAuth } from './useAuth'
import { getGlobalDedup } from './useRequestDedup'
import type { PlayTime, Schedule, Song } from '~~/types'
export const useSongs = () => {
const { isAuthenticated, user, getAuthConfig, isAdmin } = useAuth()
const dedup = getGlobalDedup()
const songs = ref<Song[]>([])
const publicSchedules = ref<Schedule[]>([])
const publicSongs = ref<Song[]>([])
const loading = ref(false)
const error = ref('')
const notification = ref({ show: false, message: '', type: '' })
const similarSongFound = ref<Song | null>(null)
const playTimes = ref<PlayTime[]>([])
const playTimeEnabled = ref(false)
const songCount = ref(0)
// 显示通知
const showNotification = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
// 使用全局通知
if (window.$showNotification) {
window.$showNotification(message, type)
} else {
// 备用方案
notification.value = {
show: true,
message,
type
}
// 3秒后自动关闭
setTimeout(() => {
notification.value.show = false
}, 3000)
}
}
// 获取播放时段列表
const fetchPlayTimes = async () => {
loading.value = true
error.value = ''
try {
const response = await $fetch('/api/play-times')
playTimeEnabled.value = response.enabled
// 确保类型兼容性
if (response.playTimes && Array.isArray(response.playTimes)) {
playTimes.value = response.playTimes.map((pt) => ({
id: pt.id,
name: pt.name,
startTime: pt.startTime || undefined,
endTime: pt.endTime || undefined,
enabled: pt.enabled,
description: pt.description || undefined
}))
} else {
playTimes.value = []
}
return response
} catch (err: any) {
error.value = err.message || '获取播放时段失败'
return { enabled: false, playTimes: [] }
} finally {
loading.value = false
}
}
// 获取歌曲列表
const fetchSongs = async (
silent = false,
semester?: string,
forceRefresh = false,
bypassCache = false
) => {
if (!silent) {
loading.value = true
}
error.value = ''
try {
const requestParams = semester ? { semester } : undefined
const response = await dedup.dedupedRequest(
'songs',
async () => {
// 构建URL参数
const params = new URLSearchParams()
if (semester) {
params.append('semester', semester)
}
// 只有当 bypassCache 为 true 时才添加 bypass_cache 参数
if (bypassCache) {
params.append('bypass_cache', 'true')
}
if (forceRefresh) {
params.append('t', Date.now().toString())
}
const url = `/api/songs${params.toString() ? '?' + params.toString() : ''}`
// API请求
return await $fetch(url, {
...getAuthConfig()
})
},
requestParams,
forceRefresh
)
// 正确解析API返回的数据结构
if (response && response.success && response.data && Array.isArray(response.data.songs)) {
songs.value = response.data.songs as Song[]
} else {
songs.value = []
console.warn('API返回的数据格式不正确:', response)
}
} catch (err: any) {
error.value = err.message || '获取歌曲列表失败'
} finally {
if (!silent) {
loading.value = false
}
}
}
// 静默刷新歌曲列表 - 不显示加载状态
const refreshSongsSilent = async () => {
return fetchSongs(true)
}
// 从排期数据中提取歌曲信息
const extractSongsFromSchedules = (schedules: Schedule[]): Song[] => {
const songsMap = new Map<string, Song>()
schedules.forEach((schedule) => {
if (schedule.song) {
const songId = String(schedule.song.id)
if (!songsMap.has(songId)) {
// 将排期中的歌曲信息转换为完整的Song对象
const completeSong: Song = {
id: schedule.song.id,
title: schedule.song.title,
artist: schedule.song.artist,
requester: schedule.song.requester,
requesterGrade: schedule.song.requesterGrade || null,
requesterClass: schedule.song.requesterClass || null,
requesterId: 0, // 默认值,公共API不提供这个信息
voteCount: schedule.song.voteCount,
played: schedule.song.played || false,
playedAt: schedule.song.playedAt || null,
semester: schedule.song.semester || null,
requestedAt: schedule.song.requestedAt || new Date().toISOString(),
cover: schedule.song.cover || null,
musicPlatform: schedule.song.musicPlatform || null,
musicId: schedule.song.musicId || null
}
songsMap.set(songId, completeSong)
}
}
})
return Array.from(songsMap.values())
}
// 获取公共排期(无需登录)
const fetchPublicSchedules = async (
silent = false,
semester?: string,
forceRefresh = false,
bypassCache = false
) => {
if (!silent) {
loading.value = true
}
error.value = ''
try {
const requestParams = semester ? { semester } : undefined
const data = await dedup.dedupedRequest(
'public-schedules',
async () => {
// 构建URL参数
const params = new URLSearchParams()
if (semester) {
params.append('semester', semester)
}
// 只有当 bypassCache 为 true 时才添加 bypass_cache 参数
if (bypassCache) {
params.append('bypass_cache', 'true')
}
if (forceRefresh) {
params.append('t', Date.now().toString())
}
const url = `/api/songs/public${params.toString() ? '?' + params.toString() : ''}`
const response = await $fetch(url, {
...getAuthConfig()
})
return response
},
requestParams,
forceRefresh
)
// 确保每个排期的歌曲都有played属性,并处理null/undefined转换
const processedData = data.map((schedule: any) => {
// 处理歌曲属性
if (schedule.song && schedule.song.played === undefined) {
schedule.song.played = false
}
// 处理播放时间属性
let processedPlayTime = null
if (schedule.playTime) {
processedPlayTime = {
...schedule.playTime,
startTime: schedule.playTime.startTime || undefined,
endTime: schedule.playTime.endTime || undefined
}
}
// 返回符合Schedule类型的对象
return {
...schedule,
playTime: processedPlayTime
} as Schedule
})
publicSchedules.value = processedData
// 直接从排期数据中提取歌曲信息,避免重复请求
publicSongs.value = extractSongsFromSchedules(processedData)
} catch (err: any) {
error.value = err.message || '获取排期失败'
} finally {
if (!silent) {
loading.value = false
}
}
}
// 静默刷新公共排期 - 不显示加载状态
const refreshSchedulesSilent = async () => {
return fetchPublicSchedules(true)
}
// 字符串相似度计算(编辑距离)
const calculateSimilarity = (str1: string, str2: string): number => {
const len1 = str1.length
const len2 = str2.length
if (len1 === 0) return len2
if (len2 === 0) return len1
const matrix = Array(len1 + 1)
.fill(null)
.map(() => Array(len2 + 1).fill(null))
for (let i = 0; i <= len1; i++) matrix[i][0] = i
for (let j = 0; j <= len2; j++) matrix[0][j] = j
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // 删除
matrix[i][j - 1] + 1, // 插入
matrix[i - 1][j - 1] + cost // 替换
)
}
}
const maxLen = Math.max(len1, len2)
return (maxLen - matrix[len1][len2]) / maxLen
}
// 标准化字符串(去除标点符号、空格,转换为小写)
const normalizeString = (str: string): string => {
return str
.toLowerCase()
.replace(/[\s\-_\(\)\[\]【】()「」『』《》〈〉""''""''、,。!?:;~·]/g, '')
.replace(/[&&]/g, 'and')
.replace(/[feat\.?|ft\.?]/gi, '')
.trim()
}
// 获取当前学期名称(从数据库)
const getCurrentSemesterName = async () => {
try {
const response = await fetch('/api/semesters/current')
if (!response.ok) {
throw new Error('获取当前学期失败')
}
const data = await response.json()
return data?.name || null
} catch (error) {
console.error('获取当前学期失败:', error)
return null
}
}
// 检查相似歌曲
const checkSimilarSongs = async (title: string, artist: string): Promise<Song[]> => {
similarSongFound.value = null
const normalizedTitle = normalizeString(title)
const normalizedArtist = normalizeString(artist)
// 获取当前学期名称
const currentSemesterName = await getCurrentSemesterName()
// 1. 检查完全相同的歌曲(标准化后)
const exactSongs = songs.value.filter((song) => {
const songTitle = normalizeString(song.title)
const songArtist = normalizeString(song.artist)
const titleMatch = songTitle === normalizedTitle && songArtist === normalizedArtist
// 如果有当前学期信息,只检查当前学期的歌曲
if (currentSemesterName) {
return titleMatch && song.semester === currentSemesterName
}
// 如果没有学期信息,检查所有歌曲(向后兼容)
return titleMatch
})
if (exactSongs.length > 0) {
return exactSongs
}
// 2. 检查高度相似的歌曲
const similarSongs: Array<{ song: Song; similarity: number }> = []
songs.value.forEach((song) => {
// 如果有当前学期信息,只检查当前学期的歌曲
if (currentSemesterName && song.semester !== currentSemesterName) {
return
}
const songTitle = normalizeString(song.title)
const songArtist = normalizeString(song.artist)
// 计算标题相似度
const titleSimilarity = calculateSimilarity(normalizedTitle, songTitle)
// 计算艺术家相似度
const artistSimilarity = calculateSimilarity(normalizedArtist, songArtist)
// 检查是否为高度相似
const isHighlySimilar =
// 标题完全相同,艺术家高度相似
(titleSimilarity >= 0.95 && artistSimilarity >= 0.8) ||
// 标题高度相似,艺术家完全相同
(titleSimilarity >= 0.8 && artistSimilarity >= 0.95) ||
// 标题和艺术家都高度相似
(titleSimilarity >= 0.85 && artistSimilarity >= 0.85) ||
// 标题包含关系且艺术家相似
((songTitle.includes(normalizedTitle) || normalizedTitle.includes(songTitle)) &&
artistSimilarity >= 0.8 &&
Math.abs(songTitle.length - normalizedTitle.length) <= 3)
if (isHighlySimilar) {
const overallSimilarity = titleSimilarity * 0.7 + artistSimilarity * 0.3
similarSongs.push({ song, similarity: overallSimilarity })
}
})
// 3. 按相似度排序并返回
if (similarSongs.length > 0) {
const sortedSimilar = similarSongs
.sort((a, b) => b.similarity - a.similarity)
.map((item) => item.song)
similarSongFound.value = sortedSimilar[0] // 保持兼容性
return sortedSimilar
}
// 4. 如果没有高度相似的,检查可能的相似歌曲(降低阈值)
const possibleSimilar: Array<{ song: Song; similarity: number }> = []
songs.value.forEach((song) => {
// 如果有当前学期信息,只检查当前学期的歌曲
if (currentSemesterName && song.semester !== currentSemesterName) {
return
}
const songTitle = normalizeString(song.title)
const songArtist = normalizeString(song.artist)
const titleSimilarity = calculateSimilarity(normalizedTitle, songTitle)
const artistSimilarity = calculateSimilarity(normalizedArtist, songArtist)
// 检查可能相似的条件(更宽松)
const isPossiblySimilar =
// 标题相似度较高
(titleSimilarity >= 0.7 && artistSimilarity >= 0.6) ||
// 标题包含关系
((songTitle.includes(normalizedTitle) || normalizedTitle.includes(songTitle)) &&
artistSimilarity >= 0.5 &&
normalizedTitle.length >= 3)
if (isPossiblySimilar) {
const overallSimilarity = titleSimilarity * 0.7 + artistSimilarity * 0.3
possibleSimilar.push({ song, similarity: overallSimilarity })
}
})
if (possibleSimilar.length > 0) {
const sortedPossible = possibleSimilar
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 3) // 最多返回3个可能相似的歌曲
.map((item) => item.song)
similarSongFound.value = sortedPossible[0] // 保持兼容性
return sortedPossible
}
return []
}
// 请求歌曲
const requestSong = async (songData: {
title: string
artist: string
preferredPlayTimeId?: number | null
cover?: string | null
musicPlatform?: string | null
musicId?: string | null
playUrl?: string | null
expectedPlayDate?: string | null
expectedPlaySequence?: number | null
collaborators?: number[]
submissionNote?: string | null
submissionNotePublic?: boolean
}) => {
if (!isAuthenticated.value) {
showNotification('需要登录才能点歌', 'error')
return null
}
loading.value = true
error.value = ''
try {
// 使用认证配置
const authConfig = getAuthConfig()
const data = await $fetch('/api/songs/request', {
method: 'POST',
body: songData,
...authConfig
})
// 更新歌曲列表
await fetchSongs(true, undefined, true, true)
return data
} catch (err: any) {
const errorMsg = err.data?.message || err.message || '点歌失败'
// 如果是重复投稿错误,只显示通知而不设置全局错误
if (errorMsg.includes('已经在列表中') || errorMsg.includes('不能重复投稿')) {
showNotification(errorMsg, 'info')
} else {
error.value = errorMsg
showNotification(errorMsg, 'error')
}
return null
} finally {
loading.value = false
}
}
// 投票
const voteSong = async (songId: number | { id: number; unvote?: boolean }) => {
if (!isAuthenticated.value) {
showNotification('需要登录才能投票', 'error')
return null
}
loading.value = true
error.value = ''
// 处理传入的可能是对象的情况
const actualSongId = typeof songId === 'object' ? songId.id : songId
const isUnvote = typeof songId === 'object' && songId.unvote === true
try {
// 检查是否已经投过票(只在正常投票时检查)
const targetSong = songs.value.find((s) => s.id === actualSongId)
if (!isUnvote && targetSong && targetSong.voted) {
showNotification('您已经为这首歌投过票了', 'info')
loading.value = false
return null
}
// 使用认证配置
const authConfig = getAuthConfig()
try {
const data = await $fetch('/api/songs/vote', {
method: 'POST',
body: {
songId: actualSongId,
unvote: isUnvote // 添加取消投票参数
},
...authConfig
})
// 立即更新本地歌曲的投票状态
if (targetSong) {
if (isUnvote) {
// 取消投票
targetSong.voted = false
targetSong.voteCount = Math.max(0, (targetSong.voteCount || 1) - 1)
showNotification(`已取消对歌曲《${targetSong.title}》的投票`, 'success')
} else {
// 正常投票
targetSong.voted = true
targetSong.voteCount = (targetSong.voteCount || 0) + 1
showNotification(`为歌曲《${targetSong.title}》投票成功!`, 'success')
}
}
return data
} catch (fetchErr: any) {
// 处理API返回的错误
const errorMsg = fetchErr.data?.message || fetchErr.message || '投票失败'
// 如果是已投票错误,只显示通知,不抛出异常
if (errorMsg.includes('已经为这首歌投过票')) {
showNotification('您已经为这首歌投过票了', 'info')
// 如果API返回已投票错误,也更新本地状态
if (targetSong && !targetSong.voted) {
targetSong.voted = true
}
return null
}
// 其他错误则抛出
throw fetchErr
}
} catch (err: any) {
const errorMsg = err.data?.message || err.message || '投票失败'
error.value = errorMsg
showNotification(errorMsg, 'error')
return null
} finally {
loading.value = false
}
}
// 撤回歌曲(只能撤回自己的投稿)
const withdrawSong = async (songId: number) => {
if (!isAuthenticated.value) {
showNotification('需要登录才能撤回歌曲', 'error')
return null
}
loading.value = true
error.value = ''
try {
// 查找歌曲信息用于通知
const targetSong = songs.value.find((s) => s.id === songId)
const songTitle = targetSong ? targetSong.title : '歌曲'
// 使用认证配置
const authConfig = getAuthConfig()
const data = await $fetch('/api/songs/withdraw', {
method: 'POST',
body: { songId },
...authConfig
})
// 更新歌曲列表
await fetchSongs(true, undefined, true, true)
// 显示成功通知
let message = ''
if (data.action === 'leave') {
message = `已成功退出歌曲《${songTitle}》的联合投稿`
} else if (data.message) {
// 如果后端返回了特定消息,优先使用(除了撤回投稿的情况,我们要保留带标题的格式)
message = data.message
} else {
message = data.quotaReturned
? `已成功撤回《${songTitle}》的投稿,投稿配额已返还`
: `已成功撤回《${songTitle}》的投稿`
}
if (data.action !== 'leave') {
message = data.quotaReturned
? `已成功撤回《${songTitle}》的投稿,投稿配额已返还`
: `已成功撤回《${songTitle}》的投稿`
}
showNotification(message, 'success')
return data
} catch (err: any) {
const errorMsg = err.data?.message || err.message || '撤回歌曲失败'
error.value = errorMsg
showNotification(errorMsg, 'error')
return null
} finally {
loading.value = false
}
}
// 删除歌曲(管理员专用)
const deleteSong = async (songId: number) => {
if (!isAuthenticated.value) {
showNotification('需要登录才能删除歌曲', 'error')
return null
}
loading.value = true
error.value = ''
try {
// 使用认证配置
const authConfig = getAuthConfig()
const data = await $fetch('/api/admin/songs/delete', {
method: 'POST',
body: { songId },
...authConfig
})
// 更新歌曲列表和公开排期
await Promise.all([
fetchSongs(true, undefined, true, true),
fetchPublicSchedules(true, undefined, true, true)
])
showNotification('歌曲已成功删除!', 'success')
return data
} catch (err: any) {
const errorMsg = err.data?.message || err.message || '删除歌曲失败'
error.value = errorMsg
showNotification(errorMsg, 'error')
return null
} finally {
loading.value = false
}
}
// 标记歌曲为已播放(管理员专用)
const markPlayed = async (songId: number) => {
if (!isAuthenticated.value) {
showNotification('需要登录才能标记歌曲', 'error')
return null
}
loading.value = true
error.value = ''
try {
// 使用认证配置
const authConfig = getAuthConfig()
const data = await $fetch('/api/admin/songs/mark-played', {
method: 'POST',
body: { songId },
...authConfig
})
// 更新歌曲列表和公开排期
await Promise.all([
fetchSongs(true, undefined, true, true),
fetchPublicSchedules(true, undefined, true, true)
])
showNotification('歌曲已成功标记为已播放!', 'success')
return data
} catch (err: any) {
const errorMsg = err.data?.message || err.message || '标记歌曲失败'
error.value = errorMsg
showNotification(errorMsg, 'error')
return null
} finally {
loading.value = false
}
}
// 撤回歌曲已播放状态(管理员专用)
const unmarkPlayed = async (songId: number) => {
if (!isAuthenticated.value) {
showNotification('需要登录才能撤回歌曲已播放状态', 'error')
return null
}
loading.value = true
error.value = ''
try {
// 使用认证配置
const authConfig = getAuthConfig()
const data = await $fetch('/api/admin/songs/mark-played', {
method: 'POST',
body: { songId, unmark: true },
...authConfig
})
// 更新歌曲列表和公开排期
await Promise.all([
fetchSongs(true, undefined, true, true),
fetchPublicSchedules(true, undefined, true, true)
])
showNotification('歌曲已成功撤回已播放状态!', 'success')
return data
} catch (err: any) {
const errorMsg = err.data?.message || err.message || '撤回歌曲已播放状态失败'
error.value = errorMsg
showNotification(errorMsg, 'error')
return null
} finally {
loading.value = false
}
}
// 申请重播
const requestReplay = async (songId: number) => {
if (!isAuthenticated.value) {
showNotification('需要登录才能申请重播', 'error')
return null
}
loading.value = true
error.value = ''
try {
const authConfig = getAuthConfig()
const data = await $fetch('/api/songs/replay', {
method: 'POST',
body: { songId },
...authConfig
})
// 更新本地数据状态
const songIndex = songs.value.findIndex((s) => s.id === songId)
if (songIndex !== -1) {
songs.value[songIndex].replayRequested = true
}
// 更新歌曲列表
await fetchSongs(true, undefined, true, true)
showNotification('重播申请已提交,请等待管理员审核', 'success')
return data
} catch (err: any) {
const errorMsg = err.data?.message || err.message || '申请重播失败'
if (errorMsg.includes('已经申请')) {
showNotification('您已经申请过重播这首歌了', 'info')
} else {
showNotification(errorMsg, 'error')
}
return null
} finally {
loading.value = false
}
}
/**
* 撤回重播申请
* @param songId 歌曲ID
*/
const withdrawReplay = async (songId: number) => {
const { user } = useAuth()
if (!user.value) {
showNotification('需要登录才能取消重播申请', 'error')
return null
}
loading.value = true
error.value = ''
try {
const authConfig = getAuthConfig()
const data = await $fetch('/api/songs/replay', {
method: 'DELETE',
body: { songId },
...authConfig
})
// 更新歌曲列表
await fetchSongs(true, undefined, true, true)
showNotification('已取消重播申请', 'success')
return data
} catch (err: any) {
const errorMsg = err.data?.message || err.message || '取消重播申请失败'
showNotification(errorMsg, 'error')
return null
} finally {
loading.value = false
}
}
// 按热度排序的歌曲
const songsByPopularity = computed(() => {
return [...songs.value].sort((a, b) => b.voteCount - a.voteCount)
})
// 按创建时间排序的歌曲
const songsByDate = computed(() => {
return [...songs.value].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
})
// 已播放的歌曲
const playedSongs = computed(() => {
return songs.value.filter((song) => song.played)
})
// 未播放的歌曲
const unplayedSongs = computed(() => {
return songs.value.filter((song) => !song.played)
})
// 我的投稿歌曲
const mySongs = computed(() => {
if (!user.value) return []
return songs.value.filter((song) => song.requesterId === user.value?.id)
})
// 我的重播申请歌曲
const myReplaySongs = computed(() => {
if (!user.value) return []
return songs.value.filter((song) => song.replayRequested)
})
// 所有可见的歌曲(登录用户看到的 + 公共歌曲)
const visibleSongs = computed(() => {
if (songs.value && songs.value.length > 0) {
return songs.value
} else {
return publicSongs.value
}
})
// 根据播放时间段过滤歌曲排期
const filterSchedulesByPlayTime = (schedules: Schedule[], playTimeId: number | null) => {
if (playTimeId === null) {
return schedules.filter((s) => s.playTimeId === null)
}
return schedules.filter((s) => s.playTimeId === playTimeId)
}
// 获取播放时间名称
const getPlayTimeName = (playTimeId: number | null) => {
if (playTimeId === null) {
return '未指定时段'
}
const foundTime = playTimes.value.find((pt) => pt.id === playTimeId)
return foundTime ? foundTime.name : '未知时段'
}
// 格式化播放时间显示
const formatPlayTimeDisplay = (playTime: PlayTime | null) => {
if (!playTime) {
return '全天'
}
let displayText = playTime.name
// 如果有开始和结束时间,添加时间信息
if (playTime.startTime && playTime.endTime) {
displayText += ` (${playTime.startTime}-${playTime.endTime})`
}
// 如果只有开始时间
else if (playTime.startTime) {
displayText += ` (${playTime.startTime}起)`
}
// 如果只有结束时间
else if (playTime.endTime) {
displayText += ` (至${playTime.endTime})`
}
return displayText
}
// 获取歌曲总数(缓存版本)
const fetchSongCount = async (forceRefresh = false) => {
try {
const response = await dedup.dedupedRequest('song-count', async () => {
const response = await $fetch(`/api/songs/count${forceRefresh ? '?t=' + Date.now() : ''}`)
return response
}, undefined, forceRefresh)
// 正确解析API返回的数据结构
if (response && typeof response.count === 'number') {
songCount.value = response.count
return response.count
} else {
console.warn('歌曲总数API返回的数据格式不正确:', response)
songCount.value = 0
return 0
}
} catch (err: any) {
console.error('获取歌曲总数失败:', err)
return 0
}
}
// 初始化加载
const initialize = async () => {
await fetchPlayTimes()
if (isAuthenticated.value) {
await fetchSongs()
} else {
await fetchPublicSchedules()
}
}
return {
songs,
publicSongs,
publicSchedules,
visibleSongs,
songCount,
loading,
error,
notification,
similarSongFound,
playTimes,
playTimeEnabled,
showNotification,
fetchSongs,
fetchPublicSchedules,
fetchPlayTimes,
fetchSongCount,
refreshSongsSilent,
refreshSchedulesSilent,
checkSimilarSongs,
requestSong,
voteSong,
withdrawSong,
deleteSong,
markPlayed,
unmarkPlayed,
requestReplay,
withdrawReplay,
filterSchedulesByPlayTime,
getPlayTimeName,
formatPlayTimeDisplay,
extractSongsFromSchedules,
initialize,
songsByPopularity,
songsByDate,
playedSongs,
unplayedSongs,
mySongs,
myReplaySongs
}
}