change file structure

This commit is contained in:
2025-07-25 18:00:17 +09:00
parent 32cd5b9be8
commit b48464f5cf
42 changed files with 42968 additions and 28647 deletions

View File

@@ -0,0 +1 @@
BROWSER=none

View File

@@ -0,0 +1,11 @@
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"semi": true,
"useTabs": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}

View File

@@ -0,0 +1 @@
module.exports = [require.resolve('./.webpack.config.js')]

View File

@@ -0,0 +1,5 @@
// define child rescript
module.exports = config => {
config.target = 'electron-renderer';
return config;
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Dong-Hyun Kim
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,2 @@
# ChartMelonPlayer
Melon Chart Player

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
{
"name": "chartmelonplayer",
"version": "0.1.0",
"private": true,
"dependencies": {
"antd": "^5.26.5",
"electron-is-dev": "^1.1.0",
"emotion": "^10.0.23",
"frameless-titlebar": "^1.0.8",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-scripts": "^5.0.1"
},
"main": "public/electron.js",
"scripts": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test",
"eject": "react-scripts eject",
"api": "cd src/lib && go run Apiserve.go",
"app": "concurrently \"npm run api\" \"npm run start\" \"wait-on http://localhost:3000 && electron .\""
},
"eslintConfig": {
"parser": "babel-eslint",
"extends": [
"airbnb"
],
"plugins": [
"react",
"jsx-a11y",
"import"
],
"rules": {
"linebreak-style": 0,
"import/no-extraneous-dependencies": 0,
"no-use-before-define": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/click-events-have-key-events": 0
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@rescripts/cli": "^0.0.3",
"@rescripts/rescript-env": "^0.0.11",
"axios": "^1.10.0",
"babel-eslint": "^10.0.3",
"concurrently": "^5.0.0",
"electron": "^37.2.1",
"electron-builder": "^26.0.12",
"eslint": "6.1.0",
"eslint-config-airbnb": "18.0.1",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.14.3",
"eslint-plugin-react-hooks": "1.7.0",
"prettier-eslint": "^16.4.2",
"prop-types": "^15.7.2",
"wait-on": "^8.0.3"
}
}

View File

@@ -0,0 +1,45 @@
const electron = require('electron');
const { app } = electron;
const { BrowserWindow } = electron;
const path = require('path');
const isDev = require('electron-is-dev');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 680,
frame: false,
titleBarStyle: 'hidden',
webPreferences: {
nodeIntegration: true,
webSecurity: false,
},
});
mainWindow.loadURL(
isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`,
);
if (isDev) {
// Open the DevTools.
// BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
mainWindow.webContents.openDevTools();
}
mainWindow.on('closed', () => { mainWindow = null; });
}
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>ChartMelon Player</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View File

@@ -0,0 +1,6 @@
import React from 'react';
import Charting from './Components/Charting';
const App = () => <Charting />;
export default App;

View File

@@ -0,0 +1,78 @@
import React, { useState } from 'react';
import axios from 'axios';
import TitleBar from 'frameless-titlebar';
import { css } from 'emotion';
import { Button, Menu } from 'antd';
import SongItem from './SongItem';
const Charting = () => {
const [chartData, setChartData] = useState({});
const [loading, setLoading] = useState(false);
const [isloaded, setIsLoaded] = useState(false);
const loadData = async () => {
const { data } = await axios.get('http://localhost:3001/api');
setChartData(data);
setLoading(false);
setIsLoaded(true);
};
const syncLoading = () => {
setLoading(true);
loadData();
};
const regenLoading = () => {
// setLoading(true);
};
return (
<>
<TitleBar app="&nbsp;ChartMelon Player" />
<Menu mode="horizontal">
<Menu.Item disabled>
<Button type="primary" loading={loading} onClick={syncLoading}>
Sync
</Button>
&nbsp;
<Button type="danger" loading={false} onClick={regenLoading}>
DB Regen
</Button>
</Menu.Item>
</Menu>
<div className={style}>
{
isloaded
? <SongItem data={chartData} />
: <span>차트를 불러오시려면 Sync 버튼을 눌러주세요!</span>
}
</div>
</>
);
};
const style = css`
overflow-y: auto;
overflow-x: hidden;
position: absolute;
top: 76px;
bottom: 0;
left: 0;
right: 0;
margin-left: 10px;
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(90, 90, 90);
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
`;
export default Charting;

View File

@@ -0,0 +1,43 @@
import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { List, Avatar } from 'antd';
const SongItem = ({ data }) => {
const [initLoading, setInitLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const initLoadCallBack = useCallback(() => {
setInitLoading(false);
if (data) {
setIsLoading(true);
}
}, [data]);
useEffect(initLoadCallBack, [data, initLoadCallBack]);
return (isLoading
? (
<List
className="song-chart"
loading={initLoading}
itemLayout="horizontal"
dataSource={data}
renderItem={(src) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={src.Img} />}
title={src.Name}
description={`${src.Artist.substring(0, src.Artist.length / 2)} [${src.Album}]`}
/>
</List.Item>
)}
/>
) : null
);
};
SongItem.propTypes = {
data: PropTypes.object.isRequired,
};
export default SongItem;

View File

@@ -0,0 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import 'antd/dist/antd.css';
ReactDOM.render(<App />, document.getElementById('root'));

View File

@@ -0,0 +1,114 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
)
type Song struct {
Img string
Name string
Artist string
Album string
}
func parseMelon() []string {
// Request the HTML Page.
res, err := http.Get("https://www.melon.com/chart/index.htm")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
log.Fatalf("Status Code Error: %d %s", res.StatusCode, res.Status)
}
// Load the HTML DOM
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
log.Fatal(err)
}
var data []string
var img_data []string
// Find the Song items
doc.Find("div.ellipsis").Each(func(i int, s *goquery.Selection) {
// For Each item Found, Get the Band and Title
some := s.Find("a").Text()
data = append(data, some)
})
data = data[6:]
if strings.Contains(data[0], "재생") {
data = data[1:]
}
fmt.Println("len:", len(data))
// Find the Song img items
doc.Find("img").Each(func(i int, s *goquery.Selection) {
// For Each item Found, Get the Band and Title
value, isExist := s.Attr("src")
if isExist {
result := strings.Replace(value, "/melon/resize/120/quality/80/optimize", "", 1)
img_data = append(img_data, result)
}
})
img_data = img_data[26:]
img_data = img_data[:len(img_data) - 8]
if strings.Contains(img_data[0], "btn_next.png") {
img_data = img_data[1:]
}
fmt.Println("data: ", img_data)
fmt.Println("len:", len(img_data))
for i := 0; i < 100; i++ {
temp := append([]string{img_data[i]}, data[i + (3 * i):]...)
data = append(data[:i + (3 * i)], temp...)
}
return data
}
func defaultHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Println("default : ", r.Form)
fmt.Println("path", r.URL.Path)
fmt.Println("param : ", r.Form["test_param"])
for k, v := range r.Form {
fmt.Println("key : ", k)
fmt.Println("val : ", strings.Join(v, ""))
}
var data = parseMelon()
songs := []Song{}
fmt.Println("len:", len(data))
// Not Clean Artist - Artist * 2
for i := 0; i < 400; i += 4 {
sng := Song{Img: data[i], Name: data[i + 1], Artist: data[i + 2], Album: data[i + 3]}
songs = append(songs, sng)
}
doc, _ := json.Marshal(songs)
fmt.Fprintf(w, string(doc))
}
func main() {
http.HandleFunc("/api", defaultHandler)
err := http.ListenAndServe(":3001", nil)
if err != nil {
log.Fatal("ListenAndServe : ", err)
} else {
fmt.Println("ListenAndServe Started! -> Port(3001)")
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
// .babelrc
{
"presets" : ["@babel/preset-env"]
}

View File

@@ -0,0 +1,21 @@
{
"name": "FloChartAPIServer",
"version": "1.0.0",
"description": "FloChart-API-Server",
"main": "index.js",
"license": "MIT",
"dependencies": {
"apollo-server": "^2.17.0",
"axios": "^0.20.0",
"graphql": "^15.3.0"
},
"scripts": {
"start": "nodemon --exec babel-node src/index.js"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/node": "^7.10.5",
"@babel/preset-env": "^7.11.5",
"nodemon": "^2.0.4"
}
}

View File

@@ -0,0 +1,3 @@
const songlists = [];
export default songlists;

View File

@@ -0,0 +1,30 @@
import songlists from '../database/songlists';
const resolvers = {
Query: {
songs: () => songlists,
song: (_, { rank }) => {
return songlists.filter(song => song.rank === rank)[0];
}
},
Mutation: {
addSong: (_, { name, artist, album, img }) => {
if (songlists.find(song => song.name === name)) return null;
const newSong = {
id : songlists.length + 1,
rank: songlists.length + 1,
name: name,
artist: artist,
album: album,
img: img
};
songlists.push(newSong);
return newSong;
}
}
}
export default resolvers;

View File

@@ -0,0 +1,23 @@
import { gql } from 'apollo-server';
const typeDefs = gql`
type Song {
id : Int!
rank: Int!
name: String!
artist: String!
album: String!
img: String!
}
type Query {
songs: [Song!]!
song(id: Int!): Song
}
type Mutation {
addSong(name: String!, artist: String!, album: String!, img: String!): Song!
}
`;
export default typeDefs;

View File

@@ -0,0 +1,14 @@
import { ApolloServer } from 'apollo-server';
import resolvers from './graphql/resolvers';
import typeDefs from './graphql/typeDefs';
// ApolloServer는 스키마와 리졸버가 반드시 필요함
const server = new ApolloServer({
typeDefs,
resolvers
});
// listen 함수로 웹 서버 실행
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"semi": true,
"useTabs": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}

View File

@@ -0,0 +1 @@
module.exports = [require.resolve('./.webpack.config.js')]

View File

@@ -0,0 +1,5 @@
// define child rescript
module.exports = config => {
config.target = 'electron-renderer';
return config;
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Dong-Hyun Kim
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,3 @@
# FloChart Player
...TODO - Use YT API

View File

@@ -0,0 +1,73 @@
{
"name": "floChart-player",
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.1.4",
"@apollo/react-hooks": "^4.0.0",
"antd": "^3.25.1",
"apollo-boost": "^0.4.9",
"electron-is-dev": "^1.1.0",
"emotion": "^10.0.23",
"frameless-titlebar": "^1.0.8",
"graphql": "^15.3.0",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-scripts": "3.2.0"
},
"main": "public/electron.js",
"scripts": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test",
"eject": "react-scripts eject",
"app": "concurrently \"yarn start\" \"wait-on http://localhost:3000 && electron .\""
},
"eslintConfig": {
"parser": "babel-eslint",
"extends": [
"airbnb"
],
"plugins": [
"react",
"jsx-a11y",
"import"
],
"rules": {
"linebreak-style": 0,
"import/no-extraneous-dependencies": 0,
"no-use-before-define": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/click-events-have-key-events": 0
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@rescripts/cli": "^0.0.13",
"@rescripts/rescript-env": "^0.0.11",
"axios": "^0.19.0",
"concurrently": "^5.0.0",
"electron": "^7.1.1",
"electron-builder": "^22.1.0",
"eslint": "6.1.0",
"eslint-config-airbnb": "18.0.1",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.14.3",
"eslint-plugin-react-hooks": "1.7.0",
"prettier-eslint": "^9.0.0",
"prop-types": "^15.7.2",
"wait-on": "^3.3.0"
}
}

View File

@@ -0,0 +1,45 @@
const electron = require('electron');
const { app } = electron;
const { BrowserWindow } = electron;
const path = require('path');
const isDev = require('electron-is-dev');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 680,
frame: false,
titleBarStyle: 'hidden',
webPreferences: {
nodeIntegration: true,
webSecurity: false,
},
});
mainWindow.loadURL(
isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`,
);
if (isDev) {
// Open the DevTools.
// BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
mainWindow.webContents.openDevTools();
}
mainWindow.on('closed', () => { mainWindow = null; });
}
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>FloChart Player</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View File

@@ -0,0 +1,6 @@
import React from 'react';
import Charting from './Components/Charting';
const App = () => <Charting />;
export default App;

View File

@@ -0,0 +1,114 @@
import React, { useState } from 'react';
import axios from 'axios';
import TitleBar from 'frameless-titlebar';
import { css } from 'emotion';
import { Button, Menu } from 'antd';
import SongItem from './SongItem';
import { gql, useMutation } from '@apollo/client';
import { useQuery } from '@apollo/react-hooks';
const ADD_SONG = gql`
mutation AddSong($name: String!, $artist: String!, $album: String!, $img: String!) {
addSong(name: $name, artist: $artist, album: $album, img: $img) {
id
name
artist
album
img
}
}
`;
const GET_SONGLIST = gql`
{
songs {
rank
name
artist
album
img
}
}
`;
const Charting = () => {
const [addSong, { songdata }] = useMutation(ADD_SONG);
const [chartData, setChartData] = useState({});
const [loading, setLoading] = useState(false);
const [isloaded, setIsLoaded] = useState(false);
const { loading_, error, data } = useQuery(GET_SONGLIST);
const jsonLoading = async () => {
const { data } = await axios.get('https://www.music-flo.com/api/meta/v1/chart/track/1');
setChartData(data.data);
console.log("ok.");
};
const syncLoading = () => {
setLoading(true);
setLoading(false);
console.log(data?.songs);
setIsLoaded(true);
};
const regenLoading = async () => {
chartData.trackList.forEach(song => {
console.log(song.name, song.artistList[0].name, song.album.title, song.album.imgList[5].url);
addSong({ variables: { name: song.name, artist: song.artistList[0].name, album: song.album.title, img: song.album.imgList[5].url } });
});
};
return (
<>
<TitleBar app="&nbsp;FloChart Player" />
<Menu mode="horizontal">
<Menu.Item disabled>
<Button type="primary" loading={loading} onClick={syncLoading}>
Sync
</Button>
&nbsp;
<Button type="danger" loading={false} onClick={regenLoading}>
DB Regen
</Button>
&nbsp;
<Button type="primary" loading={false} onClick={jsonLoading}>
Get JSON
</Button>
</Menu.Item>
</Menu>
<div className={style}>
{
isloaded
? <SongItem data={data?.songs} />
: <span>차트를 불러오시려면 Sync 버튼을 눌러주세요!</span>
}
</div>
</>
);
};
const style = css`
overflow-y: auto;
overflow-x: hidden;
position: absolute;
top: 76px;
bottom: 0;
left: 0;
right: 0;
margin-left: 10px;
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(90, 90, 90);
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
`;
export default Charting;

View File

@@ -0,0 +1,43 @@
import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { List, Avatar } from 'antd';
const SongItem = ({ data }) => {
const [initLoading, setInitLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const initLoadCallBack = useCallback(() => {
setInitLoading(false);
if (data) {
setIsLoading(true);
}
}, [data]);
useEffect(initLoadCallBack, [data, initLoadCallBack]);
return (isLoading
? (
<List
className="song-chart"
loading={initLoading}
itemLayout="horizontal"
dataSource={data}
renderItem={(src) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={src.img} />}
title={src.name}
description={`${src.name} - ${src.artist} [${src.album}]`}
/>
</List.Item>
)}
/>
) : null
);
};
SongItem.propTypes = {
data: PropTypes.object.isRequired,
};
export default SongItem;

View File

@@ -0,0 +1,7 @@
import ApolloClient from 'apollo-boost';
const client = new ApolloClient({
uri: 'http://localhost:4000/'
});
export default client;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import 'antd/dist/antd.css';
import { ApolloProvider } from '@apollo/react-hooks';
import client from './apollo';
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);

File diff suppressed because it is too large Load Diff