diff --git a/.github/workflows/scrape.yaml b/.github/workflows/scrape.yaml new file mode 100644 index 0000000..a342e91 --- /dev/null +++ b/.github/workflows/scrape.yaml @@ -0,0 +1,63 @@ +name: Daily Scrape + +on: + workflow_dispatch: + schedule: + - cron: "30 5 * * *" + +jobs: + scrape: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run scraper + run: | + # Add your scraping command here + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + + - name: Restore NuGet packages + shell: bash + run: | + dotnet restore + + - name: Build solution + shell: bash + run: | + dotnet build -c Release + + - name: Install Playwright + shell: pwsh + run: | + $playwright = Get-ChildItem -File Microsoft.Playwright.dll -Path . -Recurse + $installer = "$($playwright[0].Directory.FullName)/playwright.ps1" + & "$installer" install + + - name: Run scraper app - Daily 100 + shell: pwsh + run: | + $date = (Get-Date).ToUniversalTime().AddHours(9).ToString("yyyyMMdd") + $result = dotnet run --project ./samples/MelonChart.ConsoleApp/ -- -c Daily100 --json | ConvertFrom-Json + + mkdir -p ./data + pushd ./data + $result | ConvertTo-Json -Depth 100 | Out-File -FilePath "daily100-$date.json" -Force + popd + + - name: Upload data + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Update data" + branch: "main" + commit_user_name: "GitHub Actions" + commit_user_email: "scraper+github-actions[bot]@users.noreply.github.com" + commit_author: "GitHub Actions " diff --git a/samples/MelonChart.ConsoleApp/Options/ArgumentOptions.cs b/samples/MelonChart.ConsoleApp/Options/ArgumentOptions.cs index 1f73a62..e67a90b 100644 --- a/samples/MelonChart.ConsoleApp/Options/ArgumentOptions.cs +++ b/samples/MelonChart.ConsoleApp/Options/ArgumentOptions.cs @@ -1,57 +1,66 @@ -namespace MelonChart.ConsoleApp.Options; - -/// -/// This represents the options entity for arguments. -/// -public class ArgumentOptions -{ - /// - /// Gets or sets the value. - /// - public ChartTypes ChartType { get; set; } = ChartTypes.Top100; - - /// - /// Gets or sets the value indicating whether to display help or not. - /// - public bool Help { get; set; } = false; - - /// - /// Parses the arguments and returns the instance. - /// - /// List of arguments. - /// Returns the instance. - public static ArgumentOptions Parse(string[] args) - { - var options = new ArgumentOptions(); - if (args.Length == 0) - { - return options; - } - - for (var i = 0; i < args.Length; i++) - { - var arg = args[i]; - switch (arg) - { - case "-c": - case "-t": - case "--chart": - case "--type": - case "--chart-type": - options.ChartType = i < args.Length - 1 - ? Enum.TryParse(args[++i], ignoreCase: true, out var result) - ? result - : throw new ArgumentException("Invalid chart type. It should be 'Top100', 'Hot100', 'Daily100', 'Weekly100' or 'Monthly100'.") - : throw new ArgumentException("Invalid chart type. It should be 'Top100', 'Hot100', 'Daily100', 'Weekly100' or 'Monthly100'."); - break; - - case "-h": - case "--help": - options.Help = true; - break; - } - } - - return options; - } -} +namespace MelonChart.ConsoleApp.Options; + +/// +/// This represents the options entity for arguments. +/// +public class ArgumentOptions +{ + /// + /// Gets or sets the value. + /// + public ChartTypes ChartType { get; set; } = ChartTypes.Top100; + + /// + /// Gets or sets the value indicating whether to output as JSON or not. + /// + public bool OutputAsJson { get; set; } = false; + + /// + /// Gets or sets the value indicating whether to display help or not. + /// + public bool Help { get; set; } = false; + + /// + /// Parses the arguments and returns the instance. + /// + /// List of arguments. + /// Returns the instance. + public static ArgumentOptions Parse(string[] args) + { + var options = new ArgumentOptions(); + if (args.Length == 0) + { + return options; + } + + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + switch (arg) + { + case "-c": + case "-t": + case "--chart": + case "--type": + case "--chart-type": + options.ChartType = i < args.Length - 1 + ? Enum.TryParse(args[++i], ignoreCase: true, out var result) + ? result + : throw new ArgumentException("Invalid chart type. It should be 'Top100', 'Hot100', 'Daily100', 'Weekly100' or 'Monthly100'.") + : throw new ArgumentException("Invalid chart type. It should be 'Top100', 'Hot100', 'Daily100', 'Weekly100' or 'Monthly100'."); + break; + + case "--json": + options.OutputAsJson = true; + break; + + case "-h": + case "--help": + options.Help = true; + break; + } + } + + return options; + } +} diff --git a/samples/MelonChart.ConsoleApp/Services/MelonChartService.cs b/samples/MelonChart.ConsoleApp/Services/MelonChartService.cs index 6a5fb23..59eac37 100644 --- a/samples/MelonChart.ConsoleApp/Services/MelonChartService.cs +++ b/samples/MelonChart.ConsoleApp/Services/MelonChartService.cs @@ -1,84 +1,115 @@ -using MelonChart.Abstractions; -using MelonChart.ConsoleApp.Options; -using MelonChart.Models; - -namespace MelonChart.ConsoleApp.Services; - -/// -/// This represents the service entity for Melon chart. -/// -/// List of instances. -public class MelonChartService(IEnumerable charts) : IMelonChartService -{ - private readonly IEnumerable _charts = charts ?? throw new ArgumentNullException(nameof(charts)); - - /// - public async Task RunAsync(string[] args) - { - var options = ArgumentOptions.Parse(args); - if (options.Help) - { - this.DisplayHelp(); - return; - } - - try - { - var chart = this._charts.SingleOrDefault(p => p.ChartType.Equals(options.ChartType)); - if (chart is null) - { - throw new ArgumentException("Invalid chart type. It should be 'Top100', 'Hot100', 'Daily100', 'Weekly100' or 'Monthly100'."); - } - - var collection = await chart.GetChartAsync().ConfigureAwait(false); - this.DisplayDetails(collection); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - this.DisplayHelp(); - } - } - - private void DisplayDetails(ChartItemCollection collection) - { - Console.WriteLine($"Chart Type: {collection.ChartType}"); - switch (collection.ChartType) - { - case ChartTypes.Top100: - case ChartTypes.Hot100: - default: - Console.WriteLine($"Date/Time: {collection.DateLastUpdated} {collection.TimeLastUpdated}"); - break; - - case ChartTypes.Daily100: - Console.WriteLine($"Date: {collection.DateLastUpdated}"); - break; - - case ChartTypes.Weekly100: - Console.WriteLine($"Week: {collection.PeriodFrom} - {collection.PeriodTo}"); - break; - - case ChartTypes.Monthly100: - Console.WriteLine($"Month: {collection.Year}-{collection.Month}"); - break; - } - Console.WriteLine(); - - var items = collection.Items; - - Console.WriteLine("Rank\tTitle\tArtist\tAlbum"); - Console.WriteLine("----\t-----\t------\t-----"); - foreach (var item in items) - { - Console.WriteLine($"{item.Rank}\t{item.Title}\t{item.Artist}\t{item.Album}"); - } - } - - private void DisplayHelp() - { - Console.WriteLine("Usage:"); - Console.WriteLine(" -c, -t, --chart, --type, --chart-type Chart type - 'Top100', 'Hot100', 'Daily100', 'Weekly100' or 'Monthly100'."); - Console.WriteLine(" -h, --help Display help"); - } -} +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +using MelonChart.Abstractions; +using MelonChart.ConsoleApp.Options; +using MelonChart.Models; + +namespace MelonChart.ConsoleApp.Services; + +/// +/// This represents the service entity for Melon chart. +/// +/// List of instances. +public class MelonChartService(IEnumerable charts) : IMelonChartService +{ + private readonly IEnumerable _charts = charts ?? throw new ArgumentNullException(nameof(charts)); + + private static JsonSerializerOptions jso = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, + }; + + /// + public async Task RunAsync(string[] args) + { + var options = ArgumentOptions.Parse(args); + if (options.Help) + { + this.DisplayHelp(); + return; + } + + try + { + var chart = this._charts.SingleOrDefault(p => p.ChartType.Equals(options.ChartType)); + if (chart is null) + { + throw new ArgumentException("Invalid chart type. It should be 'Top100', 'Hot100', 'Daily100', 'Weekly100' or 'Monthly100'."); + } + + var collection = await chart.GetChartAsync().ConfigureAwait(false); + if (options.OutputAsJson) + { + Console.WriteLine(JsonSerializer.Serialize(collection, jso)); + return; + } + + this.DisplayDetails(collection); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + this.DisplayHelp(); + } + } + + private void DisplayDetails(ChartItemCollection collection) + { + Console.WriteLine($"Chart Type: {collection.ChartType}"); + switch (collection.ChartType) + { + case ChartTypes.Top100: + case ChartTypes.Hot100: + default: + Console.WriteLine($"Date/Time: {collection.DateLastUpdated} {collection.TimeLastUpdated}"); + break; + + case ChartTypes.Daily100: + Console.WriteLine($"Date: {collection.DateLastUpdated}"); + break; + + case ChartTypes.Weekly100: + Console.WriteLine($"Week: {collection.PeriodFrom} - {collection.PeriodTo}"); + break; + + case ChartTypes.Monthly100: + Console.WriteLine($"Month: {collection.Year}-{collection.Month}"); + break; + } + Console.WriteLine(); + + var items = collection.Items; + + Console.WriteLine("Rank\tStatus\tTitle\tArtist\tAlbum"); + Console.WriteLine("----\t-----\t-----\t------\t-----"); + foreach (var item in items) + { + Console.WriteLine($"{item.Rank}\t{this.GetRankStatus(item)}\t{item.Title}\t{item.Artist}\t{item.Album}"); + } + } + + private void DisplayHelp() + { + Console.WriteLine("Usage:"); + Console.WriteLine(" -c, -t, --chart, --type, --chart-type Chart type - 'Top100', 'Hot100', 'Daily100', 'Weekly100' or 'Monthly100'."); + Console.WriteLine(" --json Output in JSON format"); + Console.WriteLine(" -h, --help Display help"); + } + + private string GetRankStatus(ChartItem item) + { + return item.RankStatus switch + { + RankStatus.None => "--", + RankStatus.Up => $"+{item.RankStatusValue}", + RankStatus.Down => $"-{item.RankStatusValue}", + RankStatus.New => "new", + _ => "Unknown", + }; + } +} diff --git a/samples/MelonChart.WebApp/Components/Pages/Home.razor b/samples/MelonChart.WebApp/Components/Pages/Home.razor index 20aa07f..86e97f5 100644 --- a/samples/MelonChart.WebApp/Components/Pages/Home.razor +++ b/samples/MelonChart.WebApp/Components/Pages/Home.razor @@ -1,38 +1,42 @@ -@page "/" -@using MelonChart.WebApp.Components.UI -@using Microsoft.AspNetCore.WebUtilities -@inject NavigationManager NavManager - -Melon Chart - -
-

Melon Chart – @ChartType

- - - - -
- -@code { - protected ChartTypes ChartType { get; set; } - - protected override async Task OnInitializedAsync() - { - var uri = new Uri(NavManager.Uri); - var query = QueryHelpers.ParseQuery(uri.Query); - var chart = query.TryGetValue("chart", out var chartType) ? chartType.ToString() : "top100"; - this.ChartType = Enum.TryParse(chart, ignoreCase: true, out var result) ? result : ChartTypes.Top100; - } - - protected async Task OnClickAsync(ChartTypes chartType) - { - this.ChartType = chartType; - NavManager.NavigateTo($"?chart={chartType.ToString().ToLowerInvariant()}", forceLoad: true); - } +@page "/" +@using MelonChart.WebApp.Components.UI +@using Microsoft.AspNetCore.WebUtilities +@inject NavigationManager NavManager + +Melon Chart + +
+

Melon Chart – @ChartType

+ + + + +
+ +@code { + protected ChartTypes ChartType { get; set; } + + protected override async Task OnInitializedAsync() + { + var uri = new Uri(NavManager.Uri); + var query = QueryHelpers.ParseQuery(uri.Query); + var chart = query.TryGetValue("chart", out var chartType) ? chartType.ToString() : "top100"; + this.ChartType = Enum.TryParse(chart, ignoreCase: true, out var result) ? result : ChartTypes.Top100; + + await Task.CompletedTask; + } + + protected async Task OnClickAsync(ChartTypes chartType) + { + this.ChartType = chartType; + NavManager.NavigateTo($"?chart={chartType.ToString().ToLowerInvariant()}", forceLoad: true); + + await Task.CompletedTask; + } } \ No newline at end of file diff --git a/samples/MelonChart.WebApp/Components/UI/ChartComponent.razor b/samples/MelonChart.WebApp/Components/UI/ChartComponent.razor index b31a6bd..dfb4cd3 100644 --- a/samples/MelonChart.WebApp/Components/UI/ChartComponent.razor +++ b/samples/MelonChart.WebApp/Components/UI/ChartComponent.razor @@ -1,77 +1,94 @@ -@using MelonChart.Models -@using MelonChart.WebApp.Services -@inject IMelonChartService Chart - -
- @if (this.Collection == null) - { -

@Message

- } - else - { -

@Period

- - - - - - - - - - - @foreach (var item in this.Collection.Items) - { - - - - - - - } - -
RankTitleArtistAlbum
@item.Rank@item.Title@item.Artist@item.Album @item.Album
- } -
- -@code { - [Parameter] - public ChartTypes ChartType { get; set; } - - protected string? Message { get; set; } = "Loading..."; - protected string? Period { get; set; } - protected ChartItemCollection? Collection { get; set; } - - protected override async Task OnInitializedAsync() - { - try - { - this.Collection = await Chart.RunAsync(this.ChartType); - switch (this.Collection.ChartType) - { - case ChartTypes.Top100: - case ChartTypes.Hot100: - default: - this.Period = $"Date/Time: {this.Collection.DateLastUpdated} {this.Collection.TimeLastUpdated}"; - break; - - case ChartTypes.Daily100: - this.Period = $"Date: {this.Collection.DateLastUpdated}"; - break; - - case ChartTypes.Weekly100: - this.Period = $"Week: {this.Collection.PeriodFrom} - {this.Collection.PeriodTo}"; - break; - - case ChartTypes.Monthly100: - this.Period = $"Month: {this.Collection.Year}-{this.Collection.Month}"; - break; - } - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - this.Message = ex.Message; - } - } -} +@using MelonChart.Models +@using MelonChart.WebApp.Services +@inject IMelonChartService Chart + +
+ @if (this.Collection == null) + { +

@Message

+ } + else + { +

@Period

+ + + + + + + + + + + @foreach (var item in this.Collection.Items) + { + + + + + + + } + +
RankTitleArtistAlbum
+ @item.Rank + @switch (item.RankStatus) + { + case RankStatus.Up: + @item.RankStatusValue + break; + case RankStatus.Down: + @item.RankStatusValue + break; + case RankStatus.New: + + break; + default: + + break; + } + @item.Title@item.Artist@item.Album @item.Album
+ } +
+ +@code { + [Parameter] + public ChartTypes ChartType { get; set; } + + protected string? Message { get; set; } = "Loading..."; + protected string? Period { get; set; } + protected ChartItemCollection? Collection { get; set; } + + protected override async Task OnInitializedAsync() + { + try + { + this.Collection = await Chart.RunAsync(this.ChartType); + switch (this.Collection.ChartType) + { + case ChartTypes.Top100: + case ChartTypes.Hot100: + default: + this.Period = $"Date/Time: {this.Collection.DateLastUpdated} {this.Collection.TimeLastUpdated}"; + break; + + case ChartTypes.Daily100: + this.Period = $"Date: {this.Collection.DateLastUpdated}"; + break; + + case ChartTypes.Weekly100: + this.Period = $"Week: {this.Collection.PeriodFrom} - {this.Collection.PeriodTo}"; + break; + + case ChartTypes.Monthly100: + this.Period = $"Month: {this.Collection.Year}-{this.Collection.Month}"; + break; + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + this.Message = ex.Message; + } + } +} diff --git a/samples/MelonChart.WebApp/Components/UI/ChartComponent.razor.css b/samples/MelonChart.WebApp/Components/UI/ChartComponent.razor.css new file mode 100644 index 0000000..549c1a3 --- /dev/null +++ b/samples/MelonChart.WebApp/Components/UI/ChartComponent.razor.css @@ -0,0 +1,25 @@ +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + /* margin-right: 0.75rem; + top: -1px; */ + background-size: cover; +} + +.bi-caret-up-fill { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-caret-up-fill' viewBox='0 0 16 16'%3E%3Cpath d='m7.247 4.86-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z'/%3E%3C/svg%3E"); +} + +.bi-caret-down-fill { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-caret-down-fill' viewBox='0 0 16 16'%3E%3Cpath d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/%3E%3C/svg%3E"); +} + +.bi-box-arrow-in-left { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-box-arrow-in-left' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0z'/%3E%3Cpath fill-rule='evenodd' d='M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708z'/%3E%3C/svg%3E"); +} + +.bi-dash { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-dash' viewBox='0 0 16 16'%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8'/%3E%3C/svg%3E"); +} \ No newline at end of file diff --git a/src/MelonChart/Chart.cs b/src/MelonChart/Chart.cs index f1e2635..c507b19 100644 --- a/src/MelonChart/Chart.cs +++ b/src/MelonChart/Chart.cs @@ -1,90 +1,101 @@ -using MelonChart.Abstractions; -using MelonChart.Extensions; -using MelonChart.Models; - -using Microsoft.Playwright; - -namespace MelonChart; - -/// -/// This represents the chart entity. This must be inherited. -/// -public abstract class Chart : IChart -{ - /// - /// Gets the instance. - /// - protected IPage? Page { get; private set; } - - /// - /// Gets the instance. - /// - protected ChartItemCollection Collection { get; } = new(); - - /// - public abstract ChartTypes ChartType { get; } - - /// - /// Sets the page by filling in the content. - /// - protected abstract Task SetPageAsync(); - - /// - /// Sets the date and time of the chart. - /// - /// - protected abstract Task SetDateTimeAsync(); - - /// - public async Task GetChartAsync() - { - using var playwright = await Playwright.CreateAsync().ConfigureAwait(false); - await using var browser = await playwright.Chromium.LaunchAsync().ConfigureAwait(false); - - this.Page = await browser.NewPageAsync().ConfigureAwait(false); - - await this.SetPageAsync(); - - this.Collection.ChartType = this.ChartType; - - await this.SetDateTimeAsync(); - - var top50 = await this.Page.Locator("tr[class='lst50']").AllAsync(); - foreach (var tr in top50) - { - var songId = await tr.GetAttributeOfElementAsync("data-song-no").ConfigureAwait(false); - var rank = await tr.GetTextOfElementAsync("span[class='rank ']").ConfigureAwait(false); - var title = await tr.GetTextOfElementAsync("div[class='ellipsis rank01']", - "span", - "a").ConfigureAwait(false); - var artist = await tr.GetTextOfNthElementAsync(0, "div[class='ellipsis rank02']", - "a").ConfigureAwait(false); - var album = await tr.GetTextOfElementAsync("div[class='ellipsis rank03']", - "a").ConfigureAwait(false); - var image = await tr.GetAttributeOfElementAsync("src", "img[onerror='WEBPOCIMG.defaultAlbumImg(this);']") - .ConfigureAwait(false); - - this.Collection.Items.Add(new ChartItem(songId, rank, title, artist, album, image)); - } - - var bottom50 = await this.Page.Locator("tr[class='lst100']").AllAsync(); - foreach (var tr in bottom50) - { - var songId = await tr.GetAttributeOfElementAsync("data-song-no").ConfigureAwait(false); - var rank = await tr.GetTextOfElementAsync("span[class='rank ']").ConfigureAwait(false); - var title = await tr.GetTextOfElementAsync("div[class='ellipsis rank01']", - "span", - "a").ConfigureAwait(false); - var artist = await tr.GetTextOfNthElementAsync(0, "div[class='ellipsis rank02']", - "a").ConfigureAwait(false); - var album = await tr.GetTextOfElementAsync("div[class='ellipsis rank03']", - "a").ConfigureAwait(false); - var image = await tr.GetAttributeOfElementAsync("src", "img[onerror='WEBPOCIMG.defaultAlbumImg(this);']") - .ConfigureAwait(false); - - this.Collection.Items.Add(new ChartItem(songId, rank, title, artist, album, image)); - } - - return this.Collection; - } -} +using MelonChart.Abstractions; +using MelonChart.Extensions; +using MelonChart.Models; + +using Microsoft.Playwright; + +namespace MelonChart; + +/// +/// This represents the chart entity. This must be inherited. +/// +public abstract class Chart : IChart +{ + /// + /// Gets the instance. + /// + protected IPage? Page { get; private set; } + + /// + /// Gets the instance. + /// + protected ChartItemCollection Collection { get; } = new(); + + /// + public abstract ChartTypes ChartType { get; } + + /// + /// Sets the page by filling in the content. + /// + protected abstract Task SetPageAsync(); + + /// + /// Sets the date and time of the chart. + /// + /// + protected abstract Task SetDateTimeAsync(); + + /// + public async Task GetChartAsync() + { + using var playwright = await Playwright.CreateAsync().ConfigureAwait(false); + await using var browser = await playwright.Chromium.LaunchAsync().ConfigureAwait(false); + + this.Page = await browser.NewPageAsync().ConfigureAwait(false); + + await this.SetPageAsync(); + + this.Collection.ChartType = this.ChartType; + + await this.SetDateTimeAsync(); + + var top50 = await this.Page.Locator("tr[class='lst50']").AllAsync(); + this.Collection.Items.AddRange(await this.GetChartItemsAsync(top50).ConfigureAwait(false)); + + var bottom50 = await this.Page.Locator("tr[class='lst100']").AllAsync(); + this.Collection.Items.AddRange(await this.GetChartItemsAsync(bottom50).ConfigureAwait(false)); + + return this.Collection; + } + + private async Task> GetChartItemsAsync(IEnumerable locators) + { + var items = new List(); + foreach (var locator in locators) + { + var rankStatus = await locator.GetAttributeOfNthElementAsync("class", 2, + useFallbackValue: true, fallbackValue: "new", + selectors: [ "span[class='rank_wrap']", + "span" ]).ConfigureAwait(false); + var rankStatusValue = await locator.GetTextOfNthElementAsync(2, + useFallbackValue: true, fallbackValue: "0", + selectors: [ "span[class='rank_wrap']", + "span" ]).ConfigureAwait(false); + var songId = await locator.GetAttributeOfElementAsync("data-song-no").ConfigureAwait(false); + var rank = await locator.GetTextOfElementAsync("span[class='rank ']").ConfigureAwait(false); + var title = await locator.GetTextOfElementAsync("div[class='ellipsis rank01']", + "span", + "a").ConfigureAwait(false); + var artist = await locator.GetTextOfNthElementAsync(0, selectors: [ "div[class='ellipsis rank02']", + "a" ]).ConfigureAwait(false); + var album = await locator.GetTextOfElementAsync("div[class='ellipsis rank03']", + "a").ConfigureAwait(false); + var image = await locator.GetAttributeOfElementAsync("src", "img[onerror='WEBPOCIMG.defaultAlbumImg(this);']") + .ConfigureAwait(false); + + items.Add(new ChartItem() + { + SongId = songId, + Rank = rank, + RankStatus = Enum.TryParse(rankStatus, ignoreCase: true, out var result) ? result : RankStatus.Undefined, + RankStatusValue = Convert.ToInt32(rankStatusValue), + Title = title, + Artist = artist, + Album = album, + Image = image, + }); + } + + return items; + } +} diff --git a/src/MelonChart/Extensions/LocatorExtensions.cs b/src/MelonChart/Extensions/LocatorExtensions.cs index 2f313dc..9b5028d 100644 --- a/src/MelonChart/Extensions/LocatorExtensions.cs +++ b/src/MelonChart/Extensions/LocatorExtensions.cs @@ -38,9 +38,11 @@ public static class LocatorExtensions /// instance. /// Name of the attribute. /// Index of the element. + /// Value indicating whether to use the fallback value or not. + /// Fallback value. /// List of selectors. /// Returns the attribute value. - public static async Task GetAttributeOfNthElementAsync(this ILocator? locator, string name, int index = 0, params string[] selectors) + public static async Task GetAttributeOfNthElementAsync(this ILocator? locator, string name, int index = 0, bool useFallbackValue = false, string? fallbackValue = null, params string[] selectors) { if (locator == null) { @@ -53,7 +55,22 @@ public static class LocatorExtensions text = text.Locator(selector); } - var value = await text.Nth(index).GetAttributeAsync(name).ConfigureAwait(false); + string? value = default; + try + { + value = await text.Nth(index).GetAttributeAsync(name).ConfigureAwait(false); + } + catch + { + if (useFallbackValue) + { + value = fallbackValue; + } + else + { + throw; + } + } return value; } @@ -87,9 +104,11 @@ public static class LocatorExtensions /// /// instance. /// Index of the element. + /// Value indicating whether to use the fallback value or not. + /// Fallback value. /// List of selectors. /// Returns the element value. - public static async Task GetTextOfNthElementAsync(this ILocator? locator, int index = 0, params string[] selectors) + public static async Task GetTextOfNthElementAsync(this ILocator? locator, int index = 0, bool useFallbackValue = false, string? fallbackValue = null, params string[] selectors) { if (locator == null) { @@ -102,7 +121,22 @@ public static class LocatorExtensions text = text.Locator(selector); } - var value = await text.Nth(index).TextContentAsync().ConfigureAwait(false); + string? value = default; + try + { + value = await text.Nth(index).TextContentAsync().ConfigureAwait(false); + } + catch + { + if (useFallbackValue) + { + value = fallbackValue; + } + else + { + throw; + } + } return value; } diff --git a/src/MelonChart/Extensions/PageExtensions.cs b/src/MelonChart/Extensions/PageExtensions.cs index 30919da..d684efe 100644 --- a/src/MelonChart/Extensions/PageExtensions.cs +++ b/src/MelonChart/Extensions/PageExtensions.cs @@ -42,9 +42,11 @@ public static class PageExtensions /// instance. /// Name of the attribute. /// Index of the element. + /// Value indicating whether to use the fallback value or not. + /// Fallback value. /// List of selectors. /// Returns the attribute value. - public static async Task GetAttributeOfNthElementAsync(this IPage? page, string name, int index = 0, params string[] selectors) + public static async Task GetAttributeOfNthElementAsync(this IPage? page, string name, int index = 0, bool useFallbackValue = false, string? fallbackValue = null, params string[] selectors) { if (page == null) { @@ -63,7 +65,7 @@ public static class PageExtensions var text = page.Locator(selectors[0]); - return await text.GetAttributeOfNthElementAsync(name, index, selectors[1..]).ConfigureAwait(false); + return await text.GetAttributeOfNthElementAsync(name, index, useFallbackValue, fallbackValue, selectors[1..]).ConfigureAwait(false); } /// @@ -99,9 +101,11 @@ public static class PageExtensions /// /// instance. /// Index of the element. + /// Value indicating whether to use the fallback value or not. + /// Fallback value. /// List of selectors. /// Returns the element value. - public static async Task GetTextOfNthElementAsync(this IPage? page, int index = 0, params string[] selectors) + public static async Task GetTextOfNthElementAsync(this IPage? page, int index = 0, bool useFallbackValue = false, string? fallbackValue = null, params string[] selectors) { if (page == null) { @@ -120,6 +124,6 @@ public static class PageExtensions var text = page.Locator(selectors[0]); - return await text.GetTextOfNthElementAsync(index, selectors[1..]).ConfigureAwait(false); + return await text.GetTextOfNthElementAsync(index, useFallbackValue, fallbackValue, selectors[1..]).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/MelonChart/Models/ChartItem.cs b/src/MelonChart/Models/ChartItem.cs index 1a7ef5a..e4833de 100644 --- a/src/MelonChart/Models/ChartItem.cs +++ b/src/MelonChart/Models/ChartItem.cs @@ -1,43 +1,47 @@ -namespace MelonChart.Models; - -/// -/// This represents the model entity for chart item. -/// -/// Song ID. -/// Rank of the song. -/// Title of the song. -/// Artist who sings the song. -/// Album that the song contains. -/// Image URL of the song. -public class ChartItem(string? songId, string? rank, string? title, string? artist, string? album, string? image) -{ - /// - /// Gets the song ID. - /// - public string? SongId { get; } = songId; - - /// - /// Gets the rank of the song. - /// - public string? Rank { get; } = rank; - - /// - /// Gets the title of the song. - /// - public string? Title { get; } = title; - - /// - /// Gets the artist who sings the song. - /// - public string? Artist { get; } = artist; - - /// - /// Gets the album that the song contains. - /// - public string? Album { get; } = album; - - /// - /// Gets the image URL of the song. - /// - public string? Image { get; } = image; -} +namespace MelonChart.Models; + +/// +/// This represents the model entity for chart item. +/// +public class ChartItem +{ + /// + /// Gets or sets the song ID. + /// + public string? SongId { get; set; } + + /// + /// Gets or sets the rank of the song. + /// + public string? Rank { get; set; } + + /// + /// Gets or sets the rank up or down. + /// + public RankStatus RankStatus { get; set; } + + /// + /// Gets or sets the rank up or down value. + /// + public int? RankStatusValue { get; set; } + + /// + /// Gets or sets the title of the song. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the artist who sings the song. + /// + public string? Artist { get; set; } + + /// + /// Gets or sets the album that the song contains. + /// + public string? Album { get; set; } + + /// + /// Gets or sets the image URL of the song. + /// + public string? Image { get; set; } +} diff --git a/src/MelonChart/RankStatus.cs b/src/MelonChart/RankStatus.cs new file mode 100644 index 0000000..dd8e545 --- /dev/null +++ b/src/MelonChart/RankStatus.cs @@ -0,0 +1,32 @@ +namespace MelonChart; + +/// +/// This defines the rank status. +/// +public enum RankStatus +{ + /// + /// Identifies the rank undefined. + /// + Undefined, + + /// + /// Identifies the rank stayed. + /// + None, + + /// + /// Identifies the rank went up. + /// + Up, + + /// + /// Identifies the rank went down. + /// + Down, + + /// + /// Identifies the rank is new. + /// + New, +} \ No newline at end of file diff --git a/test/MelonChart.Tests/Extensions/LocatorExtensionsTests.cs b/test/MelonChart.Tests/Extensions/LocatorExtensionsTests.cs index 4520ef4..8dd2e5b 100644 --- a/test/MelonChart.Tests/Extensions/LocatorExtensionsTests.cs +++ b/test/MelonChart.Tests/Extensions/LocatorExtensionsTests.cs @@ -56,7 +56,7 @@ public async Task Given_Null_When_Invoked_GetAttributeOfNthElementAsync_Then_It_ { var locator = default(ILocator); - Func func = async () => await locator.GetAttributeOfNthElementAsync("aaa", 0, "bbb"); + Func func = async () => await locator.GetAttributeOfNthElementAsync("aaa", 0, selectors: ["bbb"]); await func.Should().ThrowAsync(); } @@ -66,7 +66,7 @@ public async Task Given_Null_When_Invoked_GetAttributeOfNthElementAsync_Then_It_ [DataRow(null, "title", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "button")] public async Task Given_Attribute_And_Index_And_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Return_Result(string expected, string attribute, int index, params string[] selectors) { - var result = await this._locator.GetAttributeOfNthElementAsync(attribute, index, selectors); + var result = await this._locator.GetAttributeOfNthElementAsync(attribute, index, selectors: selectors); result.Should().Be(expected); } @@ -75,7 +75,7 @@ public async Task Given_Attribute_And_Index_And_Selectors_When_Invoked_GetAttrib [DataRow("aria-expanded", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "article")] public async Task Given_Attribute_And_Index_And_NonExisting_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception(string attribute, int index, params string[] selectors) { - Func func = async () => await this._locator.GetAttributeOfNthElementAsync(attribute, index, selectors); + Func func = async () => await this._locator.GetAttributeOfNthElementAsync(attribute, index, selectors: selectors); await func.Should().ThrowAsync(); } @@ -114,7 +114,7 @@ public async Task Given_Null_When_Invoked_GetTextOfNthElementAsync_Then_It_Shoul { var locator = default(ILocator); - Func func = async () => await locator.GetTextOfNthElementAsync(0, "aaa", "bbb"); + Func func = async () => await locator.GetTextOfNthElementAsync(0, selectors: ["aaa", "bbb"]); await func.Should().ThrowAsync(); } @@ -123,7 +123,7 @@ public async Task Given_Null_When_Invoked_GetTextOfNthElementAsync_Then_It_Shoul [DataRow("Docs", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "a[class='navbar__item navbar__link']")] public async Task Given_Attribute_And_Index_And_Selectors_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Return_Result(string expected, int index, params string[] selectors) { - var result = await this._locator.GetTextOfNthElementAsync(index, selectors); + var result = await this._locator.GetTextOfNthElementAsync(index, selectors: selectors); result.Should().Be(expected); } @@ -132,7 +132,7 @@ public async Task Given_Attribute_And_Index_And_Selectors_When_Invoked_GetTextOf [DataRow(0, "div[class='navbar__inner']", "div[class='navbar__items']", "article")] public async Task Given_Attribute_And_Index_And_NonExisting_Selectors_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Throw_Exception(int index, params string[] selectors) { - Func func = async () => await this._locator.GetTextOfNthElementAsync(index, selectors); + Func func = async () => await this._locator.GetTextOfNthElementAsync(index, selectors: selectors); await func.Should().ThrowAsync(); } diff --git a/test/MelonChart.Tests/Extensions/PageExtensionsTests.cs b/test/MelonChart.Tests/Extensions/PageExtensionsTests.cs index 16a77b5..4ff7086 100644 --- a/test/MelonChart.Tests/Extensions/PageExtensionsTests.cs +++ b/test/MelonChart.Tests/Extensions/PageExtensionsTests.cs @@ -1,167 +1,167 @@ -using FluentAssertions; - -using MelonChart.Extensions; - -using Microsoft.Playwright; -using Microsoft.Playwright.MSTest; - -namespace MelonChart.Tests.Extensions; - -[TestClass] -public class PageExtensionsTests : PageTest -{ - [TestInitialize] - public async Task TestInitialize() - { - var html = await File.ReadAllTextAsync("./playwright.dev.dotnet.html"); - this.Page.SetDefaultTimeout(1000); - await this.Page.SetContentAsync(html); - } - - [TestMethod] - public async Task Given_Null_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception() - { - var page = default(IPage); - - Func func = async () => await page.GetAttributeOfElementAsync("attribute", "selector"); - - await func.Should().ThrowAsync(); - } - - [TestMethod] - public async Task Given_Page_And_No_Selector_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception() - { - Func func = async () => await this.Page.GetAttributeOfElementAsync("attribute"); - - await func.Should().ThrowAsync(); - } - - [DataTestMethod] - [DataRow("false", "aria-expanded", "div[class='navbar__inner']", "div[class='navbar__items']", "button")] - [DataRow(null, "aria-extended", "div[class='navbar__inner']", "div[class='navbar__items']", "button")] - public async Task Given_Attribute_And_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Return_Result(string expected, string attribute, params string[] selectors) - { - var result = await this.Page.GetAttributeOfElementAsync(attribute, selectors); - - result.Should().Be(expected); - } - - [DataTestMethod] - [DataRow("aria-expanded", "div[class='navbar__inner']", "div[class='navbar__items']", "article")] - public async Task Given_Attribute_And_NonExisting_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception(string attribute, params string[] selectors) - { - Func func = async () => await this.Page.GetAttributeOfElementAsync(attribute, selectors); - - await func.Should().ThrowAsync(); - } - - [TestMethod] - public async Task Given_Null_When_Invoked_GetAttributeOfNthElementAsync_Then_It_Should_Throw_Exception() - { - var page = default(IPage); - - Func func = async () => await page.GetAttributeOfNthElementAsync("attribute", 0, "selector"); - - await func.Should().ThrowAsync(); - } - - [TestMethod] - public async Task Given_Page_And_No_Selector_When_Invoked_GetAttributeOfNthElementAsync_Then_It_Should_Throw_Exception() - { - Func func = async () => await this.Page.GetAttributeOfNthElementAsync("attribute", 0); - - await func.Should().ThrowAsync(); - } - - [DataTestMethod] - [DataRow("/dotnet/", "href", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "a")] - [DataRow(null, "title", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "button")] - public async Task Given_Attribute_And_Index_And_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Return_Result(string expected, string attribute, int index, params string[] selectors) - { - var result = await this.Page.GetAttributeOfNthElementAsync(attribute, index, selectors); - - result.Should().Be(expected); - } - - [DataTestMethod] - [DataRow("aria-expanded", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "article")] - public async Task Given_Attribute_And_Index_And_NonExisting_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception(string attribute, int index, params string[] selectors) - { - Func func = async () => await this.Page.GetAttributeOfNthElementAsync(attribute, index, selectors); - - await func.Should().ThrowAsync(); - } - - [TestMethod] - public async Task Given_Null_When_Invoked_GetTextOfElementAsync_Then_It_Should_Throw_Exception() - { - var page = default(IPage); - - Func func = async () => await page.GetTextOfElementAsync("selector"); - - await func.Should().ThrowAsync(); - } - - [TestMethod] - public async Task Given_Page_And_No_Selector_When_Invoked_GetTextOfElementAsync_Then_It_Should_Throw_Exception() - { - Func func = async () => await this.Page.GetTextOfElementAsync().ConfigureAwait(false); - - await func.Should().ThrowAsync(); - } - - [DataTestMethod] - [DataRow("Playwright for .NET", "div[class='navbar__inner']", "div[class='navbar__items']", "b")] - [DataRow("", "div[class='navbar__inner']", "div[class='navbar__items']", "path")] - public async Task Given_Attribute_And_Selectors_When_Invoked_GetTextOfElementAsync_Then_It_Should_Return_Result(string expected, params string[] selectors) - { - var result = await this.Page.GetTextOfElementAsync(selectors); - - result.Should().Be(expected); - } - - [DataTestMethod] - [DataRow("div[class='navbar__inner']", "div[class='navbar__items']", "article")] - public async Task Given_Attribute_And_NonExisting_Selectors_When_Invoked_GetTextOfElementAsync_Then_It_Should_Throw_Exception(params string[] selectors) - { - Func func = async () => await this.Page.GetTextOfElementAsync(selectors); - - await func.Should().ThrowAsync(); - } - - [TestMethod] - public async Task Given_Null_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Throw_Exception() - { - var page = default(IPage); - - Func func = async () => await page.GetTextOfNthElementAsync(0, "selector"); - - await func.Should().ThrowAsync(); - } - - [TestMethod] - public async Task Given_Page_And_No_Selector_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Throw_Exception() - { - Func func = async () => await this.Page.GetTextOfNthElementAsync(0); - - await func.Should().ThrowAsync(); - } - - [DataTestMethod] - [DataRow("Docs", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "a[class='navbar__item navbar__link']")] - public async Task Given_Attribute_And_Index_And_Selectors_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Return_Result(string expected, int index, params string[] selectors) - { - var result = await this.Page.GetTextOfNthElementAsync(index, selectors); - - result.Should().Be(expected); - } - - [DataTestMethod] - [DataRow(0, "div[class='navbar__inner']", "div[class='navbar__items']", "article")] - public async Task Given_Attribute_And_Index_And_NonExisting_Selectors_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Throw_Exception(int index, params string[] selectors) - { - Func func = async () => await this.Page.GetTextOfNthElementAsync(index, selectors); - - await func.Should().ThrowAsync(); - } +using FluentAssertions; + +using MelonChart.Extensions; + +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; + +namespace MelonChart.Tests.Extensions; + +[TestClass] +public class PageExtensionsTests : PageTest +{ + [TestInitialize] + public async Task TestInitialize() + { + var html = await File.ReadAllTextAsync("./playwright.dev.dotnet.html"); + this.Page.SetDefaultTimeout(1000); + await this.Page.SetContentAsync(html); + } + + [TestMethod] + public async Task Given_Null_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception() + { + var page = default(IPage); + + Func func = async () => await page.GetAttributeOfElementAsync("attribute", "selector"); + + await func.Should().ThrowAsync(); + } + + [TestMethod] + public async Task Given_Page_And_No_Selector_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception() + { + Func func = async () => await this.Page.GetAttributeOfElementAsync("attribute"); + + await func.Should().ThrowAsync(); + } + + [DataTestMethod] + [DataRow("false", "aria-expanded", "div[class='navbar__inner']", "div[class='navbar__items']", "button")] + [DataRow(null, "aria-extended", "div[class='navbar__inner']", "div[class='navbar__items']", "button")] + public async Task Given_Attribute_And_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Return_Result(string expected, string attribute, params string[] selectors) + { + var result = await this.Page.GetAttributeOfElementAsync(attribute, selectors); + + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("aria-expanded", "div[class='navbar__inner']", "div[class='navbar__items']", "article")] + public async Task Given_Attribute_And_NonExisting_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception(string attribute, params string[] selectors) + { + Func func = async () => await this.Page.GetAttributeOfElementAsync(attribute, selectors); + + await func.Should().ThrowAsync(); + } + + [TestMethod] + public async Task Given_Null_When_Invoked_GetAttributeOfNthElementAsync_Then_It_Should_Throw_Exception() + { + var page = default(IPage); + + Func func = async () => await page.GetAttributeOfNthElementAsync("attribute", 0, selectors: ["selector"]); + + await func.Should().ThrowAsync(); + } + + [TestMethod] + public async Task Given_Page_And_No_Selector_When_Invoked_GetAttributeOfNthElementAsync_Then_It_Should_Throw_Exception() + { + Func func = async () => await this.Page.GetAttributeOfNthElementAsync("attribute", 0); + + await func.Should().ThrowAsync(); + } + + [DataTestMethod] + [DataRow("/dotnet/", "href", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "a")] + [DataRow(null, "title", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "button")] + public async Task Given_Attribute_And_Index_And_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Return_Result(string expected, string attribute, int index, params string[] selectors) + { + var result = await this.Page.GetAttributeOfNthElementAsync(attribute, index, selectors: selectors); + + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("aria-expanded", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "article")] + public async Task Given_Attribute_And_Index_And_NonExisting_Selectors_When_Invoked_GetAttributeOfElementAsync_Then_It_Should_Throw_Exception(string attribute, int index, params string[] selectors) + { + Func func = async () => await this.Page.GetAttributeOfNthElementAsync(attribute, index, selectors: selectors); + + await func.Should().ThrowAsync(); + } + + [TestMethod] + public async Task Given_Null_When_Invoked_GetTextOfElementAsync_Then_It_Should_Throw_Exception() + { + var page = default(IPage); + + Func func = async () => await page.GetTextOfElementAsync("selector"); + + await func.Should().ThrowAsync(); + } + + [TestMethod] + public async Task Given_Page_And_No_Selector_When_Invoked_GetTextOfElementAsync_Then_It_Should_Throw_Exception() + { + Func func = async () => await this.Page.GetTextOfElementAsync().ConfigureAwait(false); + + await func.Should().ThrowAsync(); + } + + [DataTestMethod] + [DataRow("Playwright for .NET", "div[class='navbar__inner']", "div[class='navbar__items']", "b")] + [DataRow("", "div[class='navbar__inner']", "div[class='navbar__items']", "path")] + public async Task Given_Attribute_And_Selectors_When_Invoked_GetTextOfElementAsync_Then_It_Should_Return_Result(string expected, params string[] selectors) + { + var result = await this.Page.GetTextOfElementAsync(selectors); + + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("div[class='navbar__inner']", "div[class='navbar__items']", "article")] + public async Task Given_Attribute_And_NonExisting_Selectors_When_Invoked_GetTextOfElementAsync_Then_It_Should_Throw_Exception(params string[] selectors) + { + Func func = async () => await this.Page.GetTextOfElementAsync(selectors); + + await func.Should().ThrowAsync(); + } + + [TestMethod] + public async Task Given_Null_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Throw_Exception() + { + var page = default(IPage); + + Func func = async () => await page.GetTextOfNthElementAsync(0, selectors: ["selector"]); + + await func.Should().ThrowAsync(); + } + + [TestMethod] + public async Task Given_Page_And_No_Selector_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Throw_Exception() + { + Func func = async () => await this.Page.GetTextOfNthElementAsync(0); + + await func.Should().ThrowAsync(); + } + + [DataTestMethod] + [DataRow("Docs", 0, "div[class='navbar__inner']", "div[class='navbar__items']", "a[class='navbar__item navbar__link']")] + public async Task Given_Attribute_And_Index_And_Selectors_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Return_Result(string expected, int index, params string[] selectors) + { + var result = await this.Page.GetTextOfNthElementAsync(index, selectors: selectors); + + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(0, "div[class='navbar__inner']", "div[class='navbar__items']", "article")] + public async Task Given_Attribute_And_Index_And_NonExisting_Selectors_When_Invoked_GetTextOfNthElementAsync_Then_It_Should_Throw_Exception(int index, params string[] selectors) + { + Func func = async () => await this.Page.GetTextOfNthElementAsync(index, selectors: selectors); + + await func.Should().ThrowAsync(); + } } \ No newline at end of file diff --git a/test/MelonChart.Tests/melonchart.sample.html b/test/MelonChart.Tests/melonchart.sample.html new file mode 100644 index 0000000..aea13f5 --- /dev/null +++ b/test/MelonChart.Tests/melonchart.sample.html @@ -0,0 +1,11159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 멜론차트>TOP100>멜론 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
skip navigation + +
+ + + + + + + +
+
+ + + + + +
+ +
+ +
+ + 2024.06.16 + + + 14:00 + +
+ + + +
+
+ +
+ + +
+
+
+ + +
+
+

+ + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
이 표는 곡 리스트로 체크박스, 순위, 곡정보, 좋아요, 뮤비, 다운, 폰전송 내용을 포함하고 있으며 표 상 하단에 제공하는 전체선택, 듣기, 다운로드, 담기, 선물하기 기능을 이용하실 수 있습니다.
+
+
+
순위
+
+
순위등락
+
+
앨범이미지
+
+
곡 상세가기
+
+
곡정보
+
+
앨범
+
+
좋아요
+
+
듣기
+
+
담기
+
+
다운
+
+
뮤비
+
1
+ + + + + + + 순위 동일 + + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Supernova +

+
+ + + aespa +
+ +
+
+ +
+ +
+ +
+ +
+ +
2
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + How Sweet +

+ + +
+
+
+
+ How Sweet +
+
+
+ +
+ +
+ +
+ +
+ +
3
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Bubble Gum +

+ + +
+
+
+
+ How Sweet +
+
+
+ +
+ +
+ +
+ +
+ +
4
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 소나기 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
5
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 고민중독 +

+
+ + + QWER +
+ +
+
+ +
+ +
+ +
+ +
+ +
6
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 해야 (HEYA) +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
7
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + SPOT! (feat. JENNIE) +

+ + +
+
+
+
+ SPOT! +
+
+
+ +
+ +
+ +
+ +
+ +
8
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Magnetic +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
9
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Armageddon +

+
+ + + aespa +
+ +
+
+ +
+ +
+ +
+ +
+ +
10
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ 2 +
+
+
+ +
+ +
+ +
+ +
+ +
11
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
12
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
13
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 미안해 미워해 사랑해 +

+
+ + + Crush +
+ +
+
+ +
+ +
+ +
+ +
+ +
14
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 천상연 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
15
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 예뻤어 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
+ +
16
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 사랑은 늘 도망가 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
17
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 에피소드 +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
18
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + SHEESH +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
19
+ + + + + + + + + 단계 상승 + + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Love wins all +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
20
+ + + + + + + + 단계 상승 + 3 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 밤양갱 +

+ + +
+
+
+
+ 밤양갱 +
+
+
+ +
+ +
+ +
+ +
+ +
21
+ + + + + + + + 단계 상승 + 3 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Hype Boy +

+ + +
+
+ +
+ +
+ +
+ +
+ +
22
+ + + + + + + 단계 하락 + 2 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 우리들의 블루스 +

+ + +
+
+
+
+ IM HERO +
+
+
+ +
+ +
+ +
+ +
+ +
23
+ + + + + + + 단계 하락 + 4 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 온기 +

+ + +
+
+
+
+ 온기 +
+
+
+ +
+ +
+ +
+ +
+ +
24
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 비의 랩소디 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
+ +
25
+ + + + + + + 단계 하락 + 3 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Home +

+ + +
+
+
+
+ 온기 +
+
+
+ +
+ +
+ +
+ +
+ +
26
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Welcome to the Show +

+ + +
+
+
+
+ Fourever +
+
+
+ +
+ +
+ +
+ +
+ +
27
+ + + + + + + 단계 하락 + 2 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 모래 알갱이 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
+ +
28
+ + + + + + + + 단계 상승 + 4 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + I AM +

+ + +
+
+
+
+ I've IVE +
+
+
+ +
+ +
+ +
+ +
+ +
29
+ + + + + + + + 단계 상승 + 4 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + ETA +

+ + +
+
+ +
+ +
+ +
+ +
+ +
30
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Seven (feat. Latto) - Clean Ver. +

+
+ + + 정국 +
+ +
+
+ +
+ +
+ +
+ +
+ +
31
+ + + + + + + 단계 하락 + 4 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 다시 만날 수 있을까 +

+ + +
+
+
+
+ IM HERO +
+
+
+ +
+ +
+ +
+ +
+ +
32
+ + + + + + + 단계 하락 + 3 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Do or Die +

+ + +
+
+
+
+ Do or Die +
+
+
+ +
+ +
+ +
+ +
+ +
33
+ + + + + + + + 단계 상승 + 4 + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
34
+ + + + + + + 단계 하락 + 4 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 이제 나만 믿어요 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
35
+ + + + + + + + 단계 상승 + 4 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Super Shy +

+ + +
+
+ +
+ +
+ +
+ +
+ +
36
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Drama +

+
+ + + aespa +
+ +
+
+ +
+ +
+ +
+ +
+ +
37
+ + + + + + + + 단계 상승 + 3 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + To. X +

+ + +
+
+ +
+ +
+ +
+ +
+ +
38
+ + + + + + + 단계 하락 + 4 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 무지개 +

+ + +
+
+
+
+ IM HERO +
+
+
+ +
+ +
+ +
+ +
+ +
39
+ + + + + + + 단계 하락 + 3 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Polaroid +

+ + +
+
+
+
+ Polaroid +
+
+
+ +
+ +
+ +
+ +
+ +
40
+ + + + + + + 단계 하락 + 5 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + London Boy +

+ + +
+
+
+
+ Polaroid +
+
+
+ +
+ +
+ +
+ +
+ +
41
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 인생찬가 +

+ + +
+
+
+
+ IM HERO +
+
+
+ +
+ +
+ +
+ +
+ +
42
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 헤어지자 말해요 +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
43
+ + + + + + + + 단계 상승 + 3 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Accendio +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
44
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + MAESTRO +

+ + +
+
+ +
+ +
+ +
+ +
+ +
45
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 슬픈 초대장 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
+ +
46
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Ditto +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
47
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Attention +

+ + +
+
+ +
+ +
+ +
+ +
+ +
48
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Spicy +

+
+ + + aespa +
+ +
+
+ +
+ +
+ +
+ +
+ +
49
+ + + + + + + 단계 하락 + 7 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 연애편지 +

+ + +
+
+
+
+ IM HERO +
+
+
+ +
+ +
+ +
+ +
+ +
50
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + WAY 4 LUV +

+
+ + + PLAVE +
+ +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
51
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
+ +
52
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 사랑인가 봐 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
53
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Lucky Girl Syndrome +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
54
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 인사 +

+
+ + + 범진 +
+ +
+
+
+
+ 인사 +
+
+
+ +
+ +
+ +
+ +
+ +
55
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 봄눈 +

+
+ + + 10CM +
+ +
+
+ +
+ +
+ +
+ +
+ +
56
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
57
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
+ +
58
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 너의 모든 순간 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
59
+ + + + + + + + 단계 상승 + 4 + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
+ +
60
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 퀸카 (Queencard) +

+ + +
+
+
+
+ I feel +
+
+
+ +
+ +
+ +
+ +
+ +
61
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + EASY +

+ + +
+
+
+
+ EASY +
+
+
+ +
+ +
+ +
+ +
+ +
62
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + OMG +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
63
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Love 119 +

+
+ + + RIIZE +
+ +
+
+
+
+ Love 119 +
+
+
+ +
+ +
+ +
+ +
+ +
64
+ + + + + + + + 단계 상승 + 4 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Dynamite +

+ + +
+
+ +
+ +
+ +
+ +
+ +
65
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Midas Touch +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
66
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Smart +

+ + +
+
+
+
+ EASY +
+
+
+ +
+ +
+ +
+ +
+ +
67
+ + + + + + + 단계 하락 + 14 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 보금자리 +

+ + +
+
+
+
+ IM HERO +
+
+
+ +
+ +
+ +
+ +
+ +
68
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + MANIAC +

+ + +
+
+ +
+ +
+ +
+ +
+ +
69
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Run Run +

+ + +
+
+ +
+ +
+ +
+ +
+ +
70
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + LOVE DIVE +

+ + +
+
+
+
+ LOVE DIVE +
+
+
+ +
+ +
+ +
+ +
+ +
71
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Love Lee +

+ + +
+
+
+
+ Love Lee +
+
+
+ +
+ +
+ +
+ +
+ +
72
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 사건의 지평선 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
73
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 주저하는 연인들을 위해 +

+ + +
+
+
+
+ 전설 +
+
+
+ +
+ +
+ +
+ +
+ +
74
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 사막에서 꽃을 피우듯 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
75
+ + + + + + + 단계 하락 + 2 + + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
76
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Get A Guitar +

+
+ + + RIIZE +
+ +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
77
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 우리 영화 +

+
+ + + PLAVE +
+ +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
78
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + +

+ + +
+
+ +
+ +
+ +
+ +
+ +
79
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 봄날 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
+ +
80
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + From +

+
+ + + PLAVE +
+ +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
81
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + I Don't Think That I Like Her +

+ + +
+
+
+
+ CHARLIE +
+
+
+ +
+ +
+ +
+ +
+ +
82
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ 항해 +
+
+
+ +
+ +
+ +
+ +
+ +
83
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
+ +
84
+ + + + + + + 단계 하락 + 2 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Watch Me Woo! +

+
+ + + PLAVE +
+ +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
85
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 취중고백 +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
86
+ + + + + + + + 단계 상승 + 3 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 손오공 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
87
+ + + + + + + 단계 하락 + 1 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 다정히 내 이름을 부르면 +

+ + + + +
+
+ +
+ +
+ +
+ +
+ +
88
+ + + + + + + + 단계 상승 + 3 + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
89
+ + + + + + + + 단계 상승 + 1 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 홀씨 +

+ + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
90
+ + + + + + + 단계 하락 + 3 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Impossible +

+
+ + + RIIZE +
+ +
+
+
+
+ RIIZING +
+
+
+ +
+ +
+ +
+ +
+ +
91
+ + + + + + + 단계 하락 + 3 + + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
92
+ + + + + + + + 단계 상승 + 2 + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 심(心) +

+ + +
+
+
+
+ 심(心) +
+
+
+ +
+ +
+ +
+ +
+ +
93
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 사랑하지 않아서 그랬어 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
94
+ + + + + + + 단계 하락 + 2 + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Baddie +

+ + +
+
+
+
+ I'VE MINE +
+
+
+ +
+ +
+ +
+ +
+ +
95
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + You & Me +

+ + +
+
+ +
+ +
+ +
+ +
+ +
96
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 청춘찬가 +

+ + +
+
+ +
+ +
+ +
+ +
+ +
97
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Siren +

+
+ + + RIIZE +
+ +
+
+
+
+ RIIZING +
+
+
+ +
+ +
+ +
+ +
+ +
98
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + Kitsch +

+ + +
+
+
+
+ I've IVE +
+
+
+ +
+ +
+ +
+ +
+ +
99
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+ +
+ +
+ +
+ +
+ +
100
+ + + + + + 순위 동일 + 0 + + + + + +
+ 곡정보 +
+
+
+ + + + + + + + + 버추얼 아이돌 +

+
+ + + PLAVE +
+ +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +