├── .github ├── ISSUE_TEMPLATE │ ├── bug-report-no-error.yaml │ ├── config.yaml │ ├── error-report.yaml │ └── feature_request.yaml ├── pull_request_template.md └── workflows │ ├── main.yml │ └── r2r.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── INSTANCES.md ├── LICENSE ├── LightTube.sln ├── LightTube ├── ApiModels │ ├── ApiChannel.cs │ ├── ApiError.cs │ ├── ApiLocals.cs │ ├── ApiPlaylist.cs │ ├── ApiResponse.cs │ ├── ApiSearchResults.cs │ ├── ApiSubscriptionInfo.cs │ ├── ApiUserData.cs │ ├── CreatePlaylistRequest.cs │ ├── HealthResponse.cs │ ├── LightTubeInstanceInfo.cs │ ├── ModifyPlaylistContentResponse.cs │ ├── Oauth2CodeGrantResponse.cs │ ├── UpdateSubscriptionRequest.cs │ └── UpdateSubscriptionResponse.cs ├── Attributes │ ├── ApiAuthorizationAttribute.cs │ ├── ApiDisableableAttribute.cs │ └── OauthApiDisableableAttribute.cs ├── Chores │ ├── ChoreManager.cs │ ├── DatabaseCleanupChore.cs │ ├── IChore.cs │ └── QueueChore.cs ├── Configuration.cs ├── Contexts │ ├── AccountContext.cs │ ├── AppearanceSettingsContext.cs │ ├── BaseContext.cs │ ├── ChannelContext.cs │ ├── ChannelsContext.cs │ ├── EmbedContext.cs │ ├── HomepageContext.cs │ ├── ImportContext.cs │ ├── LibraryContext.cs │ ├── ModalContext.cs │ ├── OAuthContext.cs │ ├── PlayerContext.cs │ ├── PlaylistContext.cs │ ├── PlaylistVideoContext.cs │ ├── SearchContext.cs │ ├── SubscriptionContext.cs │ ├── SubscriptionsContext.cs │ └── WatchContext.cs ├── Controllers │ ├── AccountController.cs │ ├── ApiController.cs │ ├── ExportController.cs │ ├── FeedController.cs │ ├── HomeController.cs │ ├── MediaController.cs │ ├── OAuth2Controller.cs │ ├── OauthApiController.cs │ ├── OpenSearchController.cs │ ├── SettingsController.cs │ └── YoutubeController.cs ├── CustomRendererDatas │ ├── EditablePlaylistVideoRendererData.cs │ └── SubscriptionFeedVideoRendererData.cs ├── Database │ ├── CacheManager.cs │ ├── DatabaseManager.cs │ ├── Models │ │ ├── DatabaseChannel.cs │ │ ├── DatabaseLogin.cs │ │ ├── DatabaseOauthToken.cs │ │ ├── DatabasePlaylist.cs │ │ ├── DatabaseUser.cs │ │ ├── DatabaseVideo.cs │ │ ├── DatabaseVideoAuthor.cs │ │ └── SubscriptionType.cs │ ├── Oauth2Manager.cs │ ├── PlaylistManager.cs │ ├── Serialization │ │ ├── BsonNullableIntSerializer.cs │ │ ├── BsonNullableStringSerializer.cs │ │ └── LightTubeBsonSerializationProvider.cs │ └── UserManager.cs ├── DropdownItem.cs ├── GuideItem.cs ├── Health │ └── HealthManager.cs ├── Importer │ ├── ImportedData.cs │ ├── ImporterUtility.cs │ └── LightTubeExport.cs ├── JsCache.cs ├── LightTube.csproj ├── Localization │ ├── Language.cs │ └── LocalizationManager.cs ├── Models │ └── ErrorViewModel.cs ├── PoToken │ ├── PoTokenData.cs │ ├── PoTokenManager.cs │ └── PoTokenResponse.cs ├── Program.cs ├── Resources │ └── Localization │ │ ├── de.json │ │ ├── en.json │ │ ├── fr.json │ │ ├── id.json │ │ ├── ja.json │ │ ├── pt.json │ │ ├── pt_BR.json │ │ ├── ro.json │ │ ├── ru.json │ │ ├── tr.json │ │ ├── vi.json │ │ └── zh_Hans.json ├── SearchAutocomplete.cs ├── SettingsTab.cs ├── SponsorBlockSegment.cs ├── Utils.cs ├── Views │ ├── Account │ │ ├── Delete.cshtml │ │ ├── Login.cshtml │ │ └── Register.cshtml │ ├── Feed │ │ ├── AddToPlaylist.cshtml │ │ ├── Channels.cshtml │ │ ├── DeletePlaylist.cshtml │ │ ├── EditPlaylist.cshtml │ │ ├── Library.cshtml │ │ ├── NewPlaylist.cshtml │ │ ├── RemoveFromPlaylist.cshtml │ │ └── Subscription.cshtml │ ├── Home │ │ ├── Error.cshtml │ │ ├── Index.cshtml │ │ └── Rss.cshtml │ ├── OAuth2 │ │ └── Authorize.cshtml │ ├── Settings │ │ ├── Account.cshtml │ │ ├── Appearance.cshtml │ │ └── ImportExport.cshtml │ ├── Shared │ │ ├── AccountDropdown.cshtml │ │ ├── ChannelTabItem.cshtml │ │ ├── GuideItem.cshtml │ │ ├── Icons.cshtml │ │ ├── Player.cshtml │ │ ├── Renderers │ │ │ ├── Channel │ │ │ │ ├── ChannelGrid.cshtml │ │ │ │ └── ChannelHome.cshtml │ │ │ ├── ContainerRenderer.cshtml │ │ │ ├── PostAttachmentRenderer.cshtml │ │ │ ├── ReelRenderer.cshtml │ │ │ ├── SearchRenderer.cshtml │ │ │ └── SearchSidebar.cshtml │ │ ├── SettingsTab.cshtml │ │ ├── SubscribeButton.cshtml │ │ ├── _AccountLayout.cshtml │ │ ├── _Layout.cshtml │ │ ├── _ModalLayout.cshtml │ │ └── _SettingsLayout.cshtml │ ├── Youtube │ │ ├── Channel.cshtml │ │ ├── Download.cshtml │ │ ├── Embed.cshtml │ │ ├── Playlist.cshtml │ │ ├── Search.cshtml │ │ ├── Subscription.cshtml │ │ └── Watch.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── YoutubeRss.cs ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── css │ ├── account.css │ ├── lighttube.css │ ├── modal.css │ └── renderers.css │ ├── favicon.ico │ ├── fonts │ └── roboto │ │ ├── LICENSE.txt │ │ ├── Roboto-Black.woff │ │ ├── Roboto-Black.woff2 │ │ ├── Roboto-BlackItalic.woff │ │ ├── Roboto-BlackItalic.woff2 │ │ ├── Roboto-Bold.woff │ │ ├── Roboto-Bold.woff2 │ │ ├── Roboto-BoldItalic.woff │ │ ├── Roboto-BoldItalic.woff2 │ │ ├── Roboto-Italic.woff │ │ ├── Roboto-Italic.woff2 │ │ ├── Roboto-Light.woff │ │ ├── Roboto-Light.woff2 │ │ ├── Roboto-LightItalic.woff │ │ ├── Roboto-LightItalic.woff2 │ │ ├── Roboto-Medium.woff │ │ ├── Roboto-Medium.woff2 │ │ ├── Roboto-MediumItalic.woff │ │ ├── Roboto-MediumItalic.woff2 │ │ ├── Roboto-Regular.woff │ │ ├── Roboto-Regular.woff2 │ │ ├── Roboto-Thin.woff │ │ ├── Roboto-Thin.woff2 │ │ ├── Roboto-ThinItalic.woff │ │ ├── Roboto-ThinItalic.woff2 │ │ └── roboto.css │ ├── icons │ ├── favicon-114x114.png │ ├── favicon-120x120.png │ ├── favicon-144x144.png │ ├── favicon-152x152.png │ ├── favicon-16x16.png │ ├── favicon-180x180.png │ ├── favicon-192x192.png │ ├── favicon-32x32.png │ ├── favicon-36x36.png │ ├── favicon-48x48.png │ ├── favicon-57x57.png │ ├── favicon-60x60.png │ ├── favicon-72x72.png │ ├── favicon-76x76.png │ ├── favicon-96x96.png │ ├── favicon-precomposed.png │ └── icon.png │ ├── js │ └── player.js │ ├── robots.txt │ └── svg │ ├── bootstrap-icons.svg │ └── loading.svg ├── OTHERLIBS.md ├── README.md ├── lighttube-helm ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml └── values.yaml ├── public_instances.json └── screenshots ├── desktop ├── dark │ ├── search.png │ └── video.png └── light │ ├── search.png │ └── video.png └── mobile ├── dark ├── search.png └── video.png └── light ├── search.png └── video.png /.github/ISSUE_TEMPLATE/bug-report-no-error.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report something that doesn't seem to work correct 3 | title: '[Bug]' 4 | labels: 5 | - bug 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Describe the bug 10 | description: Describe what exactly is going wrong 11 | placeholder: Shorts tab on channels don't work 12 | validations: 13 | required: true 14 | - type: markdown 15 | attributes: 16 | value: '# Free space, enter your comments here' 17 | - type: textarea 18 | attributes: 19 | label: Notes 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord server 4 | url: https://discord.gg/zHydENpSbr 5 | about: A place to get real-time support 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/error-report.yaml: -------------------------------------------------------------------------------- 1 | name: Error Report 2 | description: Report an error that you just encountered 3 | title: '[Error]' 4 | labels: 5 | - bug 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Describe what happened 10 | description: Describe how you managed to get the error as detailed as you can 11 | placeholder: Shorts tab on channels don't work 12 | validations: 13 | required: true 14 | - type: markdown 15 | attributes: 16 | value: '# There are some fields in the error page. Fill those values here' 17 | - type: input 18 | attributes: 19 | label: Instance URL 20 | description: The instance this error occured at 21 | placeholder: https://tube.kuylar.dev 22 | - type: input 23 | attributes: 24 | label: LightTube & InnerTube versions 25 | placeholder: 2023.01.21 / 1.0.9.0 26 | validations: 27 | required: true 28 | - type: input 29 | attributes: 30 | label: Resource path 31 | placeholder: https://tube.kuylar.dev/watch?v=dQw4w9WgXcQ 32 | validations: 33 | required: true 34 | - type: input 35 | attributes: 36 | label: Language & region 37 | placeholder: en_US 38 | validations: 39 | required: true 40 | - type: textarea 41 | attributes: 42 | label: Stack trace 43 | description: The following text (the whole thing) 44 | validations: 45 | required: true 46 | - type: markdown 47 | attributes: 48 | value: '# Free space, enter your comments here' 49 | - type: textarea 50 | attributes: 51 | label: Notes 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature, or some way to make another feature better 3 | title: '[Feature Request]' 4 | labels: 5 | - enchancement 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Description 10 | description: A clear and concise description of the problem or missing capability 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: Describe the solution you'd like 16 | description: If you have a solution in mind, please describe it. -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Details 2 | You can put detailed description of the changes in here. 3 | 4 | # Related issues/PRs 5 | Fixes #43, closes #865, etc. 6 | 7 | # Changes proposed 8 | * Outline 9 | * Your 10 | * Changes 11 | * Here 12 | 13 | # Notes 14 | Any additional notes go here. 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: publish-to-docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | publish: 10 | if: "!contains(github.event.head_commit.message, 'skip ci')" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | name: Check out code 15 | 16 | - name: Set version 17 | run: echo LIGHTTUBE_VERSION=`date +%Y%m%d` >> $GITHUB_ENV 18 | 19 | - name: Check version 20 | run: echo Building version ${{ env.LIGHTTUBE_VERSION }} 21 | - 22 | name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | with: 25 | platforms: linux/amd64,linux/arm64 26 | - 27 | name: Login to Docker Hub 28 | uses: docker/login-action@v3 29 | with: 30 | username: ${{ secrets.DOCKER_USERNAME }} 31 | password: ${{ secrets.DOCKER_PASSWORD }} 32 | - 33 | name: Build and push 34 | uses: docker/build-push-action@v5 35 | with: 36 | push: true 37 | platforms: linux/arm64,linux/amd64 38 | tags: ${{ secrets.DOCKER_USERNAME }}/lighttube:latest,${{ secrets.DOCKER_USERNAME }}/lighttube:${{ env.LIGHTTUBE_VERSION }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bin/ 3 | obj/ 4 | .vscode 5 | .vs 6 | LightTube/Properties/launchSettings.json 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at k.uylar@outlook.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to LightTube 2 | 3 | Before submitting a pull request, please make sure that it abides by the following rules: 4 | 5 | ## Proper base 6 | 7 | Make sure that your pull request is based off of the latest `master` branch. 8 | 9 | ## PR titles / content 10 | 11 | Please make sure to follow the default PR template (available in [.github/pull_request_template.md](https://github.com/lighttube-org/lighttube/blob/master/.github/pull_request_template.md)). 12 | 13 | Also, make sure that your PR titles describe what it does in a brief but understandable way. 14 | 15 | ## Commenting 16 | 17 | Make sure to document any public methods in your code using XMLDocs. 18 | 19 | ## Code style 20 | 21 | Make sure that your code follows the [Microsoft's C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions), with the following exceptions: 22 | 23 | - Using tabs over 4 spaces 24 | - Not using the `s_` or `t_` prefixes for private static fields 25 | - Implicitly typed variables are strongly discouraged 26 | 27 | # Non-code changes 28 | 29 | Make sure to prefix your PR titles with `[skip ci]` if it's not a code change (for example, documentation articles) 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | 5 | FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build 6 | ARG TARGETARCH 7 | WORKDIR /src 8 | COPY ["LightTube/LightTube.csproj", "LightTube/"] 9 | RUN dotnet restore -a $TARGETARCH "LightTube/LightTube.csproj" 10 | COPY . . 11 | WORKDIR "/src/LightTube" 12 | RUN dotnet build "LightTube.csproj" -a $TARGETARCH -c Release -o /app/build /p:Version=`date +3.%Y.%m.%d` 13 | 14 | FROM build AS publish 15 | RUN dotnet publish "LightTube.csproj" -a $TARGETARCH -c Release -o /app/publish /p:Version=`date +3.%Y.%m.%d` 16 | 17 | FROM base AS final 18 | WORKDIR /app 19 | COPY --from=publish /app/publish . 20 | RUN chmod 777 -R /tmp && chmod o+t -R /tmp 21 | CMD ASPNETCORE_URLS=http://*:$PORT dotnet LightTube.dll -------------------------------------------------------------------------------- /INSTANCES.md: -------------------------------------------------------------------------------- 1 | # This list has moved 2 | 3 | Please use the new list over at [lighttube.org](https://lighttube.org/instances) -------------------------------------------------------------------------------- /LightTube.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightTube", "LightTube\LightTube.csproj", "{279A1BE1-F31A-4BB4-9FE6-F47EDE4C8929}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {279A1BE1-F31A-4BB4-9FE6-F47EDE4C8929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {279A1BE1-F31A-4BB4-9FE6-F47EDE4C8929}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {279A1BE1-F31A-4BB4-9FE6-F47EDE4C8929}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {279A1BE1-F31A-4BB4-9FE6-F47EDE4C8929}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /LightTube/ApiModels/ApiChannel.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Models; 2 | using InnerTube.Protobuf; 3 | using InnerTube.Renderers; 4 | using LightTube.Database.Models; 5 | using LightTube.Localization; 6 | using Endpoint = InnerTube.Protobuf.Endpoint; 7 | 8 | namespace LightTube.ApiModels; 9 | 10 | public class ApiChannel 11 | { 12 | public ChannelHeader? Header { get; } 13 | public ChannelTab[] Tabs { get; } 14 | public ChannelMetadata? Metadata { get; } 15 | public RendererContainer[] Contents { get; } 16 | 17 | public ApiChannel(InnerTubeChannel channel) 18 | { 19 | Header = channel.Header; 20 | Tabs = channel.Tabs.ToArray(); 21 | Metadata = channel.Metadata; 22 | Contents = channel.Contents; 23 | } 24 | 25 | public ApiChannel(ContinuationResponse continuation) 26 | { 27 | Header = null; 28 | Tabs = []; 29 | Metadata = null; 30 | List renderers = new(); 31 | renderers.AddRange(continuation.Results); 32 | if (continuation.ContinuationToken != null) 33 | renderers.Add(new RendererContainer 34 | { 35 | Type = "continuation", 36 | OriginalType = "continuationItemRenderer", 37 | Data = new ContinuationRendererData 38 | { 39 | ContinuationToken = continuation.ContinuationToken 40 | } 41 | }); 42 | Contents = renderers.ToArray(); 43 | } 44 | 45 | public ApiChannel(DatabaseUser channel, LocalizationManager localization) 46 | { 47 | Header = new ChannelHeader(new PageHeaderRenderer 48 | { 49 | PageTitle = channel.UserID, 50 | Content = new RendererWrapper 51 | { 52 | PageHeaderViewModel = new PageHeaderViewModel 53 | { 54 | Image = new RendererWrapper 55 | { 56 | DecoratedAvatarViewModel = new DecoratedAvatarViewModel 57 | { 58 | Avatar = new RendererWrapper 59 | { 60 | AvatarViewModel = new AvatarViewModel 61 | { 62 | Image = new Image() 63 | } 64 | } 65 | }, 66 | ImageBannerViewModel = new ImageBannerViewModel 67 | { 68 | Image = new Image() 69 | } 70 | }, 71 | Metadata = new RendererWrapper 72 | { 73 | ContentMetadataViewModel = new ContentMetadataViewModel 74 | { 75 | MetadataRows = 76 | { 77 | new ContentMetadataViewModel.Types.MetadataRow 78 | { 79 | MetadataParts = 80 | { 81 | new ContentMetadataViewModel.Types.MetadataRow.Types. 82 | AttributedDescriptionWrapper 83 | { 84 | Text = new AttributedDescription 85 | { 86 | Content = $"@LT_{channel.UserID}" 87 | } 88 | } 89 | } 90 | }, 91 | new ContentMetadataViewModel.Types.MetadataRow 92 | { 93 | MetadataParts = 94 | { 95 | new ContentMetadataViewModel.Types.MetadataRow.Types. 96 | AttributedDescriptionWrapper 97 | { 98 | Text = new AttributedDescription 99 | { 100 | Content = "LightTube Channel" 101 | } 102 | }, 103 | new ContentMetadataViewModel.Types.MetadataRow.Types. 104 | AttributedDescriptionWrapper 105 | { 106 | Text = new AttributedDescription 107 | { 108 | Content = "" 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | }, 116 | Description = new RendererWrapper 117 | { 118 | DescriptionPreviewViewModel = new DescriptionPreviewViewModel 119 | { 120 | Content = new AttributedDescription 121 | { 122 | Content = "" 123 | } 124 | } 125 | } 126 | } 127 | } 128 | }, channel.LTChannelID, "en"); 129 | Tabs = 130 | [ 131 | new ChannelTab(new TabRenderer 132 | { 133 | Endpoint = new Endpoint 134 | { 135 | BrowseEndpoint = new BrowseEndpoint 136 | { 137 | Params = "EglwbGF5bGlzdHPyBgQKAkIA" 138 | } 139 | }, 140 | Title = "Playlists", 141 | Selected = true 142 | }) 143 | ]; 144 | Metadata = null; 145 | Contents = channel.PlaylistRenderers(localization).ToArray(); 146 | } 147 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/ApiError.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.ApiModels; 2 | 3 | public class ApiError(string errorMessage, int errorCode) 4 | { 5 | public string Message { get; } = errorMessage; 6 | public int Code { get; } = errorCode; 7 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/ApiLocals.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.ApiModels; 2 | 3 | public class ApiLocals 4 | { 5 | public Dictionary Languages { get; set; } 6 | public Dictionary Regions { get; set; } 7 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/ApiPlaylist.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using InnerTube.Models; 3 | using InnerTube.Protobuf; 4 | using InnerTube.Renderers; 5 | using LightTube.Database; 6 | using LightTube.Database.Models; 7 | using LightTube.Localization; 8 | using Endpoint = InnerTube.Protobuf.Endpoint; 9 | 10 | namespace LightTube.ApiModels; 11 | 12 | public class ApiPlaylist 13 | { 14 | public string Id { get; } 15 | public string[] Alerts { get; } 16 | public RendererContainer[] Contents { get; } 17 | public RendererContainer[] Chips { get; } 18 | public string? Continuation { get; } 19 | public PlaylistSidebar? Sidebar { get; } 20 | public bool Editable { get; } 21 | 22 | public ApiPlaylist(InnerTubePlaylist playlist) 23 | { 24 | Id = playlist.Id; 25 | Alerts = playlist.Alerts; 26 | Contents = playlist.Contents; 27 | Chips = playlist.Chips; 28 | Continuation = playlist.Continuation; 29 | Sidebar = playlist.Sidebar; 30 | Editable = false; 31 | } 32 | 33 | public ApiPlaylist(ContinuationResponse playlist) 34 | { 35 | Id = ""; 36 | Alerts = []; 37 | Contents = playlist.Results; 38 | Chips = []; 39 | Continuation = playlist.ContinuationToken; 40 | Sidebar = null; 41 | Editable = false; 42 | } 43 | 44 | public ApiPlaylist(DatabasePlaylist playlist, DatabaseUser author, LocalizationManager localization, DatabaseUser? user) 45 | { 46 | Id = playlist.Id; 47 | Alerts = []; 48 | Contents = DatabaseManager.Playlists.GetPlaylistVideoRenderers(playlist.Id, playlist.Author == user?.UserID, localization).ToArray(); 49 | Chips = []; 50 | Continuation = null; 51 | Sidebar = new PlaylistSidebar(playlist.GetHeaderRenderer(author, localization), "en"); 52 | Editable = playlist.Author == user?.UserID; 53 | } 54 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.ApiModels; 2 | 3 | public class ApiResponse 4 | { 5 | public string Status { get; } 6 | public ApiError? Error { get; } 7 | public T? Data { get; } 8 | public ApiUserData? UserData { get; } 9 | 10 | public ApiResponse(T data, ApiUserData? userData) 11 | { 12 | Status = "OK"; 13 | Error = null; 14 | Data = data; 15 | UserData = userData; 16 | } 17 | 18 | public ApiResponse(string status, string errorMessage, int errorCode) 19 | { 20 | Status = status; 21 | Error = new(errorMessage, errorCode); 22 | Data = default; 23 | UserData = null; 24 | } 25 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/ApiSearchResults.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using InnerTube.Models; 3 | using InnerTube.Protobuf.Params; 4 | using InnerTube.Renderers; 5 | 6 | namespace LightTube.ApiModels; 7 | 8 | public class ApiSearchResults 9 | { 10 | public RendererContainer[] Results { get; } 11 | public ShowingResultsFor? QueryCorrecter { get; } 12 | public RendererContainer[] Chips { get; } 13 | public string? Continuation { get; } 14 | public string[] Refinements { get; } 15 | public long EstimatedResults { get; } 16 | public SearchParams? SearchParams { get; } 17 | 18 | public ApiSearchResults(InnerTubeSearchResults results, SearchParams searchParams) 19 | { 20 | Results = results.Results; 21 | QueryCorrecter = results.QueryCorrecter; 22 | Chips = results.Chips; 23 | Continuation = results.Continuation; 24 | Refinements = results.Refinements; 25 | EstimatedResults = results.EstimatedResults; 26 | SearchParams = searchParams; 27 | } 28 | 29 | public ApiSearchResults(ContinuationResponse continuationResults) 30 | { 31 | Results = continuationResults.Results; 32 | QueryCorrecter = null; 33 | Chips = []; 34 | Continuation = continuationResults.ContinuationToken; 35 | Refinements = []; 36 | EstimatedResults = 0; 37 | SearchParams = null; 38 | } 39 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/ApiSubscriptionInfo.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database.Models; 2 | 3 | namespace LightTube.ApiModels; 4 | 5 | public class ApiSubscriptionInfo(SubscriptionType userSubscription) 6 | { 7 | public bool Subscribed { get; } = userSubscription != SubscriptionType.NONE; 8 | public bool Notifications { get; } = userSubscription == SubscriptionType.NOTIFICATIONS_ON; 9 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/ApiUserData.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Renderers; 2 | using LightTube.Database.Models; 3 | 4 | namespace LightTube.ApiModels; 5 | 6 | public class ApiUserData 7 | { 8 | public DatabaseUser User { get; private set; } 9 | public Dictionary Channels { get; private set; } 10 | 11 | public static ApiUserData? GetFromDatabaseUser(DatabaseUser? user) 12 | { 13 | if (user is null) return null; 14 | return new ApiUserData 15 | { 16 | User = user, 17 | Channels = [] 18 | }; 19 | } 20 | 21 | public void CalculateWithRenderers(IEnumerable renderers) 22 | { 23 | foreach (RendererContainer renderer in renderers) 24 | CalculateWithRenderer(renderer); 25 | } 26 | 27 | private void CalculateWithRenderer(RendererContainer renderer) 28 | { 29 | switch (renderer.Type) 30 | { 31 | case "channel": 32 | AddInfoForChannel((renderer.Data as ChannelRendererData)?.ChannelId); 33 | break; 34 | case "video": 35 | AddInfoForChannel((renderer.Data as VideoRendererData)?.Author?.Id); 36 | break; 37 | case "container": 38 | CalculateWithRenderers((renderer.Data as ContainerRendererData)?.Items ?? []); 39 | break; 40 | } 41 | } 42 | 43 | public void AddInfoForChannel(string? channelId) 44 | { 45 | if (channelId == null) return; 46 | if (User.Subscriptions.TryGetValue(channelId, out SubscriptionType value) && !Channels.ContainsKey(channelId)) 47 | Channels.Add(channelId, new ApiSubscriptionInfo(value)); 48 | } 49 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/CreatePlaylistRequest.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database.Models; 2 | 3 | namespace LightTube.Controllers; 4 | 5 | public class CreatePlaylistRequest 6 | { 7 | public string Title; 8 | public string? Description; 9 | public PlaylistVisibility? Visibility; 10 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/HealthResponse.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Models; 2 | 3 | namespace LightTube.ApiModels; 4 | 5 | public class HealthResponse 6 | { 7 | public int VideoHealth { get; set; } 8 | public double AveragePlayerResponseTime { get; set; } 9 | public CacheStats CacheStats { get; set; } 10 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/LightTubeInstanceInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace LightTube.ApiModels; 4 | 5 | public class LightTubeInstanceInfo 6 | { 7 | [JsonProperty("type")] public string Type { get; set; } 8 | [JsonProperty("version")] public string Version { get; set; } 9 | [JsonProperty("motd")] public string[] Messages { get; set; } 10 | [JsonProperty("alert")] public string? Alert { get; set; } 11 | [JsonProperty("config")] public Dictionary Config { get; set; } 12 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/ModifyPlaylistContentResponse.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using InnerTube.Models; 3 | 4 | namespace LightTube.Controllers; 5 | 6 | public class ModifyPlaylistContentResponse(InnerTubePlayer video) 7 | { 8 | public string Title = video.Details.Title; 9 | public string Author = video.Details.Author.Title; 10 | public string Thumbnail = $"https://i.ytimg.com/vi/{video.Details.Id}/hqdefault.jpg"; 11 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/Oauth2CodeGrantResponse.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database.Models; 2 | using Newtonsoft.Json; 3 | 4 | namespace LightTube.ApiModels; 5 | 6 | public class Oauth2CodeGrantResponse(DatabaseOauthToken token) 7 | { 8 | [JsonProperty("access_token")] public string AccessToken = token.CurrentAuthToken; 9 | [JsonProperty("token_type")] public string TokenType = "Bearer"; 10 | [JsonProperty("expires_in")] public int ExpiresIn = (int)Math.Round(token.CurrentTokenExpirationDate.Subtract(DateTimeOffset.Now).TotalSeconds); 11 | [JsonProperty("refresh_token")] public string RefreshToken = token.RefreshToken; 12 | [JsonProperty("scope")] public string Scope = string.Join(" ", token.Scopes); 13 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/UpdateSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.Controllers; 2 | 3 | public class UpdateSubscriptionRequest 4 | { 5 | public string ChannelId; 6 | public bool Subscribed; 7 | public bool EnableNotifications; 8 | } -------------------------------------------------------------------------------- /LightTube/ApiModels/UpdateSubscriptionResponse.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Models; 2 | using LightTube.ApiModels; 3 | using LightTube.Database.Models; 4 | 5 | namespace LightTube.Controllers; 6 | 7 | public class UpdateSubscriptionResponse 8 | { 9 | public string ChannelName { get; } 10 | public string ChannelAvatar { get; } 11 | public bool Subscribed { get; } 12 | public bool Notifications { get; } 13 | 14 | public UpdateSubscriptionResponse(InnerTubeChannel channel, SubscriptionType subscription) 15 | { 16 | try 17 | { 18 | ApiSubscriptionInfo info = new(subscription); 19 | Subscribed = info.Subscribed; 20 | Notifications = info.Notifications; 21 | } 22 | catch 23 | { 24 | Subscribed = false; 25 | Notifications = false; 26 | } 27 | 28 | ChannelName = channel.Metadata.Title; 29 | ChannelAvatar = channel.Metadata.AvatarUrl; 30 | } 31 | } -------------------------------------------------------------------------------- /LightTube/Attributes/ApiAuthorizationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using LightTube.ApiModels; 3 | using LightTube.Database; 4 | using LightTube.Database.Models; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | 8 | namespace LightTube.Attributes; 9 | 10 | public class ApiAuthorizationAttribute(params string[] scopes) : Attribute, IActionFilter 11 | { 12 | private string[] _scopes = scopes; 13 | 14 | public void OnActionExecuting(ActionExecutingContext context) 15 | { 16 | DatabaseOauthToken? login = DatabaseManager.Oauth2.GetLoginFromHttpContext(context.HttpContext).Result; 17 | if (login != null && _scopes.All(scope => login.Scopes.Contains(scope))) return; 18 | 19 | context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; 20 | context.Result = new JsonResult(new ApiResponse("UNAUTHORIZED", "Unauthorized", 401)); 21 | } 22 | 23 | public void OnActionExecuted(ActionExecutedContext context) 24 | { 25 | } 26 | } -------------------------------------------------------------------------------- /LightTube/Attributes/ApiDisableableAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using LightTube.Database; 3 | using LightTube.Database.Models; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | 7 | namespace LightTube.Attributes; 8 | 9 | public class ApiDisableableAttribute(params string[] scopes) : Attribute, IActionFilter 10 | { 11 | private string[] _scopes = scopes; 12 | 13 | public void OnActionExecuting(ActionExecutingContext context) 14 | { 15 | if (Configuration.ApiEnabled) return; 16 | context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; 17 | context.Result = new ContentResult(); 18 | } 19 | 20 | public void OnActionExecuted(ActionExecutedContext context) 21 | { 22 | } 23 | } -------------------------------------------------------------------------------- /LightTube/Attributes/OauthApiDisableableAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using LightTube.Database; 3 | using LightTube.Database.Models; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | 7 | namespace LightTube.Attributes; 8 | 9 | public class OauthApiDisableableAttribute(params string[] scopes) : Attribute, IActionFilter 10 | { 11 | private string[] _scopes = scopes; 12 | 13 | public void OnActionExecuting(ActionExecutingContext context) 14 | { 15 | if (Configuration.OauthEnabled) return; 16 | context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; 17 | context.Result = new ContentResult(); 18 | } 19 | 20 | public void OnActionExecuted(ActionExecutedContext context) 21 | { 22 | } 23 | } -------------------------------------------------------------------------------- /LightTube/Chores/ChoreManager.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using MongoDB.Driver.Linq; 3 | 4 | namespace LightTube.Chores; 5 | 6 | public static class ChoreManager 7 | { 8 | public static bool ChoreExecuting => _queue.Any(x => x.Value.Running); 9 | 10 | private static Dictionary _chores = []; 11 | private static Dictionary _queue = []; 12 | 13 | public static void RegisterChores() 14 | { 15 | foreach (Type choreType in Assembly.GetAssembly(typeof(ChoreManager))! 16 | .GetTypes() 17 | .Where(x => x.IsAssignableTo(typeof(IChore)))) 18 | { 19 | try 20 | { 21 | IChore chore = (IChore)Activator.CreateInstance(choreType)!; 22 | _chores.Add(chore.Id, choreType); 23 | } 24 | catch { } 25 | } 26 | } 27 | 28 | public static Guid QueueChore(string choreId) 29 | { 30 | if (!_chores.ContainsKey(choreId)) 31 | throw new KeyNotFoundException($"Unknown chore '{choreId}'"); 32 | 33 | QueueChore chore = new(_chores[choreId]); 34 | _queue.Add(chore.Id, chore); 35 | NextChore(); 36 | return chore.Id; 37 | } 38 | 39 | public static void NextChore() 40 | { 41 | if (ChoreExecuting) return; 42 | _queue.Values.FirstOrDefault(x => !x.Running && !x.Complete)?.Start(); 43 | } 44 | } -------------------------------------------------------------------------------- /LightTube/Chores/DatabaseCleanupChore.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database; 2 | using LightTube.Database.Models; 3 | using MongoDB.Driver; 4 | using MongoDB.Driver.Linq; 5 | 6 | namespace LightTube.Chores; 7 | 8 | public class DatabaseCleanupChore : IChore 9 | { 10 | public string Id => "DatabaseCleanup"; 11 | 12 | public async Task RunChore(Action updateStatus, Guid id) 13 | { 14 | List channels = []; 15 | List videos = []; 16 | List users = []; 17 | int deletedChannels = 0; 18 | int deletedVideos = 0; 19 | int deletedPlaylists = 0; 20 | int deletedTokens = 0; 21 | 22 | IAsyncCursor allUsers = await DatabaseManager.UserCollection.FindAsync(x => true); 23 | while (await allUsers.MoveNextAsync()) 24 | foreach (DatabaseUser user in allUsers.Current) 25 | { 26 | if (users.Contains(user.UserID)) 27 | updateStatus("Duplicate UserID: " + user.UserID); 28 | else 29 | users.Add(user.UserID); 30 | foreach (string channel in user.Subscriptions?.Keys.ToArray() ?? []) 31 | if (!channels.Contains(channel)) 32 | channels.Add(channel); 33 | } 34 | 35 | IAsyncCursor playlists = await DatabaseManager.PlaylistCollection.FindAsync(x => true); 36 | while (await playlists.MoveNextAsync()) 37 | foreach (DatabasePlaylist playlist in playlists.Current) 38 | { 39 | if (!users.Contains(playlist.Author)) 40 | { 41 | updateStatus($"Playlist {playlist.Name} does not belong to anyone, deleting it..."); 42 | deletedPlaylists++; 43 | await DatabaseManager.PlaylistCollection.DeleteOneAsync(x => x.Id == playlist.Id); 44 | continue; 45 | } 46 | 47 | foreach (string videoId in playlist.VideoIds) 48 | if (!videos.Contains(videoId)) 49 | videos.Add(videoId); 50 | } 51 | 52 | IMongoQueryable cachedChannels = DatabaseManager.ChannelCacheCollection.AsQueryable(); 53 | foreach (DatabaseChannel channel in cachedChannels) 54 | { 55 | if (!channels.Contains(channel.ChannelId)) 56 | { 57 | await DatabaseManager.ChannelCacheCollection.DeleteOneAsync(x => x.ChannelId == channel.ChannelId); 58 | deletedChannels++; 59 | } 60 | } 61 | 62 | updateStatus($"Deleted {deletedChannels} channels from the cache"); 63 | 64 | IMongoQueryable cachedVideos = DatabaseManager.VideoCacheCollection.AsQueryable(); 65 | foreach (DatabaseVideo video in cachedVideos) 66 | { 67 | if (!videos.Contains(video.Id)) 68 | { 69 | await DatabaseManager.VideoCacheCollection.DeleteOneAsync(x => x.Id == video.Id); 70 | deletedVideos++; 71 | } 72 | } 73 | 74 | updateStatus($"Deleted {deletedVideos} videos from the cache"); 75 | 76 | IMongoQueryable tokens = DatabaseManager.TokensCollection.AsQueryable(); 77 | foreach (DatabaseLogin login in tokens) 78 | { 79 | if (!users.Contains(login.UserID)) 80 | { 81 | await DatabaseManager.TokensCollection.DeleteOneAsync(x => x.Id == login.Id); 82 | deletedTokens++; 83 | } 84 | // 10 days difference between creation & last seen 85 | else if (login.LastSeen.Subtract(login.Created).CompareTo(TimeSpan.FromDays(10)) == 1) 86 | { 87 | // if token is older than 30 days 88 | if (login.LastSeen >= DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(30))) continue; 89 | await DatabaseManager.TokensCollection.DeleteOneAsync(x => x.Id == login.Id); 90 | deletedTokens++; 91 | } 92 | } 93 | 94 | updateStatus($"Deleted {deletedVideos} videos from the cache"); 95 | return 96 | $"deleted\n- {deletedChannels} channels\n- {deletedPlaylists} playlists\n- {deletedVideos} videos\n- {deletedTokens} tokens"; 97 | } 98 | } -------------------------------------------------------------------------------- /LightTube/Chores/IChore.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.Chores; 2 | 3 | public interface IChore 4 | { 5 | public string Id { get; } 6 | public Task RunChore(Action updateStatus, Guid id); 7 | } -------------------------------------------------------------------------------- /LightTube/Chores/QueueChore.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Serilog; 3 | 4 | namespace LightTube.Chores; 5 | 6 | public class QueueChore(Type chore) 7 | { 8 | public IChore Chore = (IChore)Activator.CreateInstance(chore)!; 9 | public Guid Id = Guid.NewGuid(); 10 | public Stopwatch Stopwatch = new(); 11 | public string Status = ""; 12 | public bool Running; 13 | public bool Complete; 14 | 15 | public void Start() 16 | { 17 | Log.Information($"[CHORE] [{Chore.Id}] Chore started"); 18 | Running = true; 19 | Stopwatch.Start(); 20 | Chore.RunChore(s => Status = s, Id) 21 | .ContinueWith(task => 22 | { 23 | Stopwatch.Stop(); 24 | Running = false; 25 | Complete = true; 26 | Log.Information($"[CHORE] [{Chore.Id}] Chore complete in {Stopwatch.Elapsed}\n{task.Result}"); 27 | ChoreManager.NextChore(); 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /LightTube/Contexts/AccountContext.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Localization; 2 | 3 | namespace LightTube.Contexts; 4 | 5 | public class AccountContext(HttpContext context) 6 | { 7 | public string? HtmlTitle { get; set; } 8 | public string? Redirect { get; set; } 9 | public string? Error { get; set; } 10 | public string? UserID { get; set; } 11 | public LocalizationManager Localization { get; set; } = LocalizationManager.GetFromHttpContext(context); 12 | } -------------------------------------------------------------------------------- /LightTube/Contexts/AppearanceSettingsContext.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using LightTube.ApiModels; 3 | using LightTube.Localization; 4 | 5 | namespace LightTube.Contexts; 6 | 7 | public class AppearanceSettingsContext( 8 | HttpContext context, 9 | ApiLocals locals, 10 | Dictionary customThemes, 11 | Language[] languages, 12 | Dictionary languagePercentages) : BaseContext(context) 13 | { 14 | public Language[] Languages = languages; 15 | public Dictionary LanguagePercentages { get; set; } = languagePercentages; 16 | public ApiLocals Locals = locals; 17 | public Dictionary CustomThemes = customThemes; 18 | public Dictionary BuiltinThemes = new() 19 | { 20 | ["auto"] = "System Default", 21 | ["light"] = "Light", 22 | ["dark"] = "Dark", 23 | }; 24 | 25 | } -------------------------------------------------------------------------------- /LightTube/Contexts/BaseContext.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database; 2 | using LightTube.Database.Models; 3 | using LightTube.Localization; 4 | using Microsoft.AspNetCore.Html; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | 7 | namespace LightTube.Contexts; 8 | 9 | public class BaseContext 10 | { 11 | public string Title; 12 | public bool IsMobile; 13 | public List HeadTags = []; 14 | public List rssElement = []; 15 | public List EndTags = []; 16 | public bool GuideHidden = false; 17 | public HttpContext Context; 18 | public DatabaseUser? User; 19 | public LocalizationManager Localization; 20 | 21 | public BaseContext(HttpContext context) 22 | { 23 | Context = context; 24 | User = DatabaseManager.Users.GetUserFromToken(context.Request.Cookies["token"] ?? "").Result; 25 | Localization = LocalizationManager.GetFromHttpContext(context); 26 | AddMeta("og:site_name", "lighttube"); 27 | AddMeta("og:type", "website"); 28 | AddMeta("theme-color", "#AA0000"); 29 | } 30 | 31 | public void AddScript(string src) 32 | { 33 | TagBuilder script = new("script"); 34 | script.Attributes.Add("src", src + "?v=" + Utils.GetVersion()); 35 | EndTags.Add(script); 36 | } 37 | 38 | public void AddStylesheet(string href) 39 | { 40 | TagBuilder stylesheet = new("link"); 41 | stylesheet.Attributes.Add("rel", "stylesheet"); 42 | stylesheet.Attributes.Add("href", href + "?v=" + Utils.GetVersion()); 43 | HeadTags.Add(stylesheet); 44 | } 45 | public void AddRSSUrl(string href) 46 | { 47 | TagBuilder rss = new("link"); 48 | rss.Attributes.Add("rel", "alternate"); 49 | rss.Attributes.Add("type", "application/rss+xml"); 50 | rss.Attributes.Add("title", "RSS"); 51 | rss.Attributes.Add("href", href); 52 | rssElement.Add(rss); 53 | } 54 | public void AddMeta(string property, string content) 55 | { 56 | TagBuilder stylesheet = new("meta"); 57 | stylesheet.Attributes.Add("property", property); 58 | stylesheet.Attributes.Add("content", content); 59 | HeadTags.Add(stylesheet); 60 | } 61 | 62 | public string? GetSearchBoxInput() 63 | { 64 | if (this is SearchContext s) return s.Query; 65 | return Context.Request.Cookies.TryGetValue("lastSearch", out string? q) ? q : null; 66 | } 67 | 68 | public string GetThemeClass() => 69 | Context.Request.Cookies.TryGetValue("theme", out string? theme) 70 | ? $"theme-{theme}" 71 | : $"theme-{Configuration.DefaultTheme}"; 72 | } -------------------------------------------------------------------------------- /LightTube/Contexts/ChannelsContext.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database.Models; 2 | 3 | namespace LightTube.Contexts; 4 | 5 | public class ChannelsContext(HttpContext context) : BaseContext(context) 6 | { 7 | public IEnumerable Channels; 8 | } -------------------------------------------------------------------------------- /LightTube/Contexts/EmbedContext.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using InnerTube.Models; 3 | 4 | namespace LightTube.Contexts; 5 | 6 | public class EmbedContext : BaseContext 7 | { 8 | public PlayerContext Player; 9 | public InnerTubeVideo Video; 10 | 11 | public EmbedContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo innerTubeNextResponse, 12 | bool compatibility, SponsorBlockSegment[] sponsors, bool audioOnly) : base(context) 13 | { 14 | Player = new PlayerContext(context, innerTubePlayer, innerTubeNextResponse, "embed", compatibility, 15 | context.Request.Query["q"], sponsors, audioOnly); 16 | Video = innerTubeNextResponse; 17 | } 18 | 19 | public EmbedContext(HttpContext context, Exception e, InnerTubeVideo innerTubeNextResponse) : base(context) 20 | { 21 | Player = new PlayerContext(context, e); 22 | Video = innerTubeNextResponse; 23 | } 24 | } -------------------------------------------------------------------------------- /LightTube/Contexts/HomepageContext.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database.Models; 2 | 3 | namespace LightTube.Contexts 4 | { 5 | public class HomepageContext : BaseContext 6 | { 7 | public FeedVideo[] Videos; 8 | public HomepageContext(HttpContext context) : base(context) 9 | { 10 | AddRSSUrl(context.Request.Scheme + "://" + context.Request.Host + "/feed/rss.xml"); 11 | if (User != null) 12 | { 13 | Videos = Task.Run(async () => 14 | { 15 | return (await YoutubeRss.GetMultipleFeeds(User.Subscriptions.Where(x => x.Value == SubscriptionType.NOTIFICATIONS_ON).Select(x => x.Key))).Take(context.Request.Cookies["maxvideos"] is null ? 5 : Convert.ToInt32(context.Request.Cookies["maxvideos"])).ToArray(); 16 | }).Result; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LightTube/Contexts/ImportContext.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.Contexts; 2 | 3 | public class ImportContext(HttpContext context, string? message = null, bool isError = false) : BaseContext(context) 4 | { 5 | public string? Message { get; } = message; 6 | public bool IsError { get; } = isError; 7 | } -------------------------------------------------------------------------------- /LightTube/Contexts/LibraryContext.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database; 2 | using LightTube.Database.Models; 3 | 4 | namespace LightTube.Contexts; 5 | 6 | public class LibraryContext : BaseContext 7 | { 8 | public IEnumerable Playlists; 9 | 10 | public LibraryContext(HttpContext context) : base(context) 11 | { 12 | Playlists = User != null 13 | ? DatabaseManager.Playlists.GetUserPlaylists(User.UserID, PlaylistVisibility.Private) 14 | : []; 15 | } 16 | } -------------------------------------------------------------------------------- /LightTube/Contexts/ModalContext.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.Contexts; 2 | 3 | public class ModalContext(HttpContext context) : BaseContext(context) 4 | { 5 | public string Title { get; set; } 6 | public ModalButton[] Buttons { get; set; } 7 | public bool AlignToStart { get; set; } 8 | } 9 | 10 | public class ModalButton(string label, string target, string type) 11 | { 12 | public string Type = type; 13 | public string Target = target; 14 | public string Label = label; 15 | } -------------------------------------------------------------------------------- /LightTube/Contexts/OAuthContext.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database; 2 | using LightTube.Database.Models; 3 | 4 | namespace LightTube.Contexts; 5 | 6 | public class OAuthContext : AccountContext 7 | { 8 | public DatabaseUser? User; 9 | public HttpContext Context; 10 | public string Name; 11 | public string[] Scopes; 12 | 13 | public OAuthContext(HttpContext context, string error, params object[] format) : base(context) 14 | { 15 | HtmlTitle = Localization.GetRawString("oauth2.title"); 16 | Error = string.Format(Localization.GetRawString(error), format); 17 | } 18 | 19 | public OAuthContext(HttpContext context, string name, string[] scopes) : base(context) 20 | { 21 | Context = context; 22 | User = DatabaseManager.Users.GetUserFromToken(context.Request.Cookies["token"] ?? "").Result; 23 | HtmlTitle = Localization.GetRawString("oauth2.title"); 24 | Name = name; 25 | Scopes = scopes; 26 | } 27 | } -------------------------------------------------------------------------------- /LightTube/Contexts/PlayerContext.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Models; 2 | using InnerTube.Protobuf; 3 | using InnerTube.Protobuf.Responses; 4 | using Newtonsoft.Json; 5 | 6 | namespace LightTube.Contexts; 7 | 8 | public class PlayerContext : BaseContext 9 | { 10 | public InnerTubePlayer? Player; 11 | public InnerTubeVideo? Video; 12 | public Exception? Exception; 13 | public bool UseHls; 14 | public bool UseDash; 15 | public Thumbnail[] Thumbnails; 16 | public string? ErrorMessage = null; 17 | public int PreferredItag = 18; 18 | public bool UseEmbedUi = false; 19 | public string? ClassName; 20 | public SponsorBlockSegment[] Sponsors; 21 | public bool AudioOnly; 22 | 23 | public PlayerContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo? video, string className, 24 | bool compatibility, string? preferredItag, SponsorBlockSegment[] sponsors, bool audioOnly) : base(context) 25 | { 26 | Player = innerTubePlayer; 27 | Video = video; 28 | ClassName = className; 29 | PreferredItag = int.TryParse(preferredItag ?? "18", out int itag) ? itag : 18; 30 | Sponsors = sponsors; 31 | UseHls = !compatibility && !string.IsNullOrWhiteSpace(innerTubePlayer.HlsManifestUrl); // Prefer HLS 32 | UseDash = innerTubePlayer.AdaptiveFormats.Any() && !compatibility; 33 | AudioOnly = audioOnly; 34 | Thumbnails = innerTubePlayer.Details.Thumbnails; 35 | // Formats 36 | if (!Configuration.ProxyEnabled) 37 | { 38 | UseHls = false; 39 | UseDash = false; 40 | } 41 | } 42 | 43 | public string GetChaptersJson() 44 | { 45 | if (Video?.Chapters is null) return "[]"; 46 | VideoChapter[] c = Video.Chapters.ToArray(); 47 | List ltChapters = []; 48 | for (int i = 0; i < c.Length; i++) 49 | { 50 | VideoChapter chapter = c[i]; 51 | float to = 100; 52 | if (i + 1 < c.Length) 53 | { 54 | VideoChapter next = c[i + 1]; 55 | to = (next.StartSeconds * 1000) / (float)Player!.Details.Length!.Value.TotalMilliseconds * 100; 56 | } 57 | ltChapters.Add(new LtVideoChapter 58 | { 59 | From = (chapter.StartSeconds * 1000) / (float)Player!.Details.Length!.Value.TotalMilliseconds * 100, 60 | To = to, 61 | Name = chapter.Title 62 | }); 63 | } 64 | 65 | return JsonConvert.SerializeObject(ltChapters); 66 | } 67 | 68 | private class LtVideoChapter 69 | { 70 | [JsonProperty("from")] public float From; 71 | [JsonProperty("to")] public float To; 72 | [JsonProperty("name")] public string Name; 73 | } 74 | 75 | public PlayerContext(HttpContext context, Exception e) : base(context) 76 | { 77 | Exception = e; 78 | Video = null!; 79 | Sponsors = []; 80 | } 81 | 82 | public int? GetFirstItag() => GetPreferredFormat()?.Itag; 83 | 84 | public Format? GetPreferredFormat() => 85 | AudioOnly 86 | ? Player?.AdaptiveFormats.FirstOrDefault(x => x.Mime.StartsWith("audio/")) 87 | : Player?.Formats.FirstOrDefault(); 88 | 89 | public string GetClass() => ClassName is not null ? $" {ClassName}" : ""; 90 | } -------------------------------------------------------------------------------- /LightTube/Contexts/PlaylistVideoContext.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Models; 2 | using LightTube.Database.Models; 3 | 4 | namespace LightTube.Contexts; 5 | 6 | public class PlaylistVideoContext : ModalContext 7 | { 8 | public string ItemId; 9 | public string ItemTitle; 10 | public string ItemSubtitle; 11 | public string ItemThumbnail; 12 | public T? Extra; 13 | 14 | public PlaylistVideoContext(HttpContext context) : base(context) 15 | { 16 | } 17 | 18 | public PlaylistVideoContext(HttpContext context, InnerTubeVideo video) : base(context) 19 | { 20 | ItemId = video.Id; 21 | ItemTitle = video.Title; 22 | ItemSubtitle = video.Channel.Title; 23 | ItemThumbnail = $"https://i.ytimg.com/vi/{video.Id}/hqdefault.jpg"; 24 | } 25 | 26 | public PlaylistVideoContext(HttpContext context, DatabaseVideo? video, string id = "") : base(context) 27 | { 28 | ItemId = (video?.Id ?? id)[..11]; 29 | ItemTitle = video?.Title ?? "Uncached Video"; 30 | ItemSubtitle = video?.Channel.Name ?? ""; 31 | ItemThumbnail = $"https://i.ytimg.com/vi/{video?.Id}/hqdefault.jpg"; 32 | } 33 | 34 | public PlaylistVideoContext(HttpContext context, DatabasePlaylist playlist) : base(context) 35 | { 36 | ItemId = playlist.Id; 37 | ItemTitle = playlist.Name; 38 | ItemSubtitle = $"{playlist.VideoIds.Count} videos"; 39 | ItemThumbnail = $"https://i.ytimg.com/vi/{playlist.VideoIds.FirstOrDefault()}/hqdefault.jpg"; 40 | } 41 | } -------------------------------------------------------------------------------- /LightTube/Contexts/SearchContext.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using InnerTube.Models; 3 | using InnerTube.Protobuf.Params; 4 | using InnerTube.Renderers; 5 | 6 | namespace LightTube.Contexts; 7 | 8 | public class SearchContext : BaseContext 9 | { 10 | public string Query; 11 | public SearchParams? Filter; 12 | public InnerTubeSearchResults? Search; 13 | public IEnumerable Results; 14 | public RendererContainer? Sidebar; 15 | public IEnumerable Chips; 16 | public string? Continuation; 17 | public int? CurrentPage; 18 | public ShowingResultsFor? QueryCorrecter; 19 | 20 | public SearchContext(HttpContext context, string query, SearchParams? filter, InnerTubeSearchResults search, 21 | int currentPage, RendererContainer? sidebar) : base(context) 22 | { 23 | Query = query; 24 | Filter = filter; 25 | Search = search; 26 | Results = Search.Results; 27 | Continuation = Search.Continuation; 28 | Chips = Search.Chips; 29 | QueryCorrecter = search.QueryCorrecter; 30 | CurrentPage = currentPage; 31 | Sidebar = sidebar; 32 | } 33 | 34 | public SearchContext(HttpContext context, string query, SearchParams? filter, SearchContinuationResponse search) : 35 | base(context) 36 | { 37 | Query = query; 38 | Filter = filter; 39 | Search = null; 40 | Results = search.Results; 41 | Continuation = search.ContinuationToken; 42 | Chips = search.Chips ?? []; 43 | QueryCorrecter = null; 44 | CurrentPage = null; 45 | Sidebar = null; 46 | } 47 | } -------------------------------------------------------------------------------- /LightTube/Contexts/SubscriptionContext.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Models; 2 | using LightTube.Database.Models; 3 | 4 | namespace LightTube.Contexts; 5 | 6 | public class SubscriptionContext : ModalContext 7 | { 8 | public InnerTubeChannel Channel; 9 | public SubscriptionType CurrentType = SubscriptionType.NONE; 10 | 11 | public SubscriptionContext(HttpContext context, InnerTubeChannel channel, SubscriptionType? subscriptionType = null) : 12 | base(context) 13 | { 14 | Channel = channel; 15 | if (!subscriptionType.HasValue) 16 | User?.Subscriptions.TryGetValue(channel.Header?.Id ?? "", out CurrentType); 17 | else 18 | CurrentType = subscriptionType.Value; 19 | Buttons = 20 | [ 21 | new ModalButton(Localization.GetRawString("subscription.edit.channel"), $"/channel/{channel.Header?.Id}", "secondary"), 22 | new ModalButton("", "|", ""), 23 | new ModalButton(Localization.GetRawString("subscription.edit.confirm"), "__submit", "primary"), 24 | ]; 25 | Title = Localization.GetRawString("subscription.edit.title"); 26 | } 27 | } -------------------------------------------------------------------------------- /LightTube/Contexts/SubscriptionsContext.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database.Models; 2 | 3 | namespace LightTube.Contexts; 4 | 5 | public class SubscriptionsContext(HttpContext context) : BaseContext(context) 6 | { 7 | public FeedVideo[] Videos; 8 | } -------------------------------------------------------------------------------- /LightTube/Controllers/ExportController.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using LightTube.Contexts; 3 | using LightTube.Database; 4 | using LightTube.Database.Models; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Newtonsoft.Json; 7 | 8 | namespace LightTube.Controllers; 9 | 10 | [Route("/export")] 11 | public class ExportController : Controller 12 | { 13 | [Route("fullData.json")] 14 | public IActionResult LightTubeExport() 15 | { 16 | BaseContext context = new(HttpContext); 17 | if (context.User == null) return Redirect("/account/login?redirectUrl=%2fexport%2ffullData.json"); 18 | 19 | LightTubeExport export = new() 20 | { 21 | Type = $"LightTube/{Utils.GetVersion()}", 22 | Host = Request.Host.ToString(), 23 | Subscriptions = [.. context.User.Subscriptions.Keys], 24 | Playlists = DatabaseManager.Playlists.GetUserPlaylists(context.User.UserID, PlaylistVisibility.Private) 25 | .Select(x => new ImportedData.Playlist 26 | { 27 | Title = x.Name, 28 | Description = x.Description, 29 | TimeCreated = null, 30 | TimeUpdated = x.LastUpdated, 31 | Visibility = x.Visibility, 32 | VideoIds = [.. x.VideoIds] 33 | }).ToArray() 34 | }; 35 | return File(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(export)), "application/json", 36 | $"LightTubeExport_{context.User.UserID}.json"); 37 | } 38 | } -------------------------------------------------------------------------------- /LightTube/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using InnerTube; 3 | using InnerTube.Protobuf.Responses; 4 | using LightTube.Contexts; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Net.Http.Headers; 7 | using Endpoint = InnerTube.Protobuf.Endpoint; 8 | 9 | namespace LightTube.Controllers; 10 | 11 | public class HomeController(SimpleInnerTubeClient innerTube) : Controller 12 | { 13 | public IActionResult Index() => View(new HomepageContext(HttpContext)); 14 | 15 | [Route("/rss")] 16 | public IActionResult Rss() => View(new BaseContext(HttpContext)); 17 | 18 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 19 | public IActionResult Error() => View(new BaseContext(HttpContext)); 20 | 21 | [Route("/css/custom.css")] 22 | public IActionResult CustomCss() 23 | { 24 | string? fileName = Configuration.CustomCssPath; 25 | 26 | if (fileName == null) return NotFound(); 27 | 28 | return File(System.IO.File.ReadAllBytes(fileName), "text/css"); 29 | } 30 | 31 | [Route("/lib/{name}")] 32 | public IActionResult CachedJs(string name) 33 | { 34 | try 35 | { 36 | return File(Encoding.UTF8.GetBytes(JsCache.GetJsFileContents(name)), 37 | name.EndsWith(".css") ? "text/css" : "text/javascript", 38 | JsCache.CacheUpdateTime, new EntityTagHeaderValue($"\"{JsCache.GetHash(name)}\"")); 39 | } 40 | catch (Exception e) 41 | { 42 | return NotFound(); 43 | } 44 | } 45 | 46 | [Route("/dismiss_alert")] 47 | public IActionResult DismissAlert(string redirectUrl) 48 | { 49 | if (Configuration.AlertHash == null) return Redirect(redirectUrl); 50 | Response.Cookies.Append("dismissedAlert", Configuration.AlertHash!, new CookieOptions 51 | { 52 | Expires = DateTimeOffset.UtcNow.AddDays(15) 53 | }); 54 | return Redirect(redirectUrl); 55 | } 56 | 57 | [Route("/{str}")] 58 | public async Task AutoRedirect(string str) 59 | { 60 | if (str.StartsWith('@')) 61 | { 62 | ResolveUrlResponse endpoint = await innerTube.ResolveUrl("https://youtube.com/" + str); 63 | return Redirect(endpoint.Endpoint.EndpointTypeCase == Endpoint.EndpointTypeOneofCase.BrowseEndpoint 64 | ? $"/channel/{endpoint.Endpoint.BrowseEndpoint.BrowseId}" 65 | : "/"); 66 | } 67 | 68 | if (str.Length == 11) 69 | return Redirect($"/watch?v={str}"); 70 | 71 | return Redirect("/"); 72 | } 73 | } -------------------------------------------------------------------------------- /LightTube/Controllers/OpenSearchController.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Web; 3 | using System.Xml; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace LightTube.Controllers; 7 | 8 | [Route("/opensearch")] 9 | public class OpenSearchController : Controller 10 | { 11 | [Route("osdd.xml")] 12 | public IActionResult OpenSearchDescriptionDocument() 13 | { 14 | XmlDocument doc = new(); 15 | 16 | XmlNode docNode = doc.CreateXmlDeclaration("1.0", "UTF-8", null); 17 | doc.AppendChild(docNode); 18 | 19 | XmlElement root = doc.CreateElement("OpenSearchDescription"); 20 | 21 | XmlElement shortName = doc.CreateElement("ShortName"); 22 | shortName.InnerText = "LightTube"; 23 | root.AppendChild(shortName); 24 | 25 | XmlElement description = doc.CreateElement("Description"); 26 | description.InnerText = "Search for videos on LightTube"; 27 | root.AppendChild(description); 28 | 29 | XmlElement inputEncoding = doc.CreateElement("InputEncoding"); 30 | inputEncoding.InnerText = "UTF-8"; 31 | root.AppendChild(inputEncoding); 32 | 33 | XmlElement image = doc.CreateElement("Image"); 34 | image.SetAttribute("width", "16"); 35 | image.SetAttribute("height", "16"); 36 | image.SetAttribute("type", "image/vnd.microsoft.icon"); 37 | image.InnerText = $"https://{Request.Host}/favicon.ico"; 38 | root.AppendChild(image); 39 | 40 | XmlElement imageHq = doc.CreateElement("Image"); 41 | imageHq.SetAttribute("width", "96"); 42 | imageHq.SetAttribute("height", "96"); 43 | imageHq.SetAttribute("type", "image/png"); 44 | imageHq.InnerText = $"https://{Request.Host}/icons/favicon-96x96.png"; 45 | root.AppendChild(imageHq); 46 | 47 | XmlElement searchUrl = doc.CreateElement("Url"); 48 | searchUrl.SetAttribute("type", "text/html"); 49 | searchUrl.SetAttribute("template", $"https://{Request.Host}/results?search_query={{searchTerms?}}"); 50 | root.AppendChild(searchUrl); 51 | 52 | XmlElement suggestionsUrl = doc.CreateElement("Url"); 53 | suggestionsUrl.SetAttribute("type", "application/x-suggestions+json"); 54 | suggestionsUrl.SetAttribute("template", $"https://{Request.Host}/opensearch/suggestions.json?q={{searchTerms?}}"); 55 | root.AppendChild(suggestionsUrl); 56 | 57 | doc.AppendChild(root); 58 | doc.DocumentElement?.SetAttribute("xmlns", "http://a9.com/-/spec/opensearch/1.1/"); 59 | doc.DocumentElement?.SetAttribute("xmlns:moz", "http://www.mozilla.org/2006/browser/search/"); 60 | 61 | return File(Encoding.UTF8.GetBytes(doc.OuterXml), "application/opensearchdescription+xml"); 62 | } 63 | 64 | [Route("suggestions.json")] 65 | public async Task Suggestions(string q, string hl = "en", string gl = "us") 66 | { 67 | object[] res = [q, new List(), new List(), new List()]; 68 | SearchAutocomplete autocomplete = await SearchAutocomplete.GetAsync(q); 69 | foreach (string s in autocomplete.Autocomplete) 70 | { 71 | (res[1] as List)!.Add(s); 72 | (res[2] as List)!.Add(""); 73 | (res[3] as List)!.Add($"https://{Request.Host}/results?search_query={HttpUtility.UrlEncode(s)}"); 74 | } 75 | 76 | return res; 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /LightTube/CustomRendererDatas/EditablePlaylistVideoRendererData.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Renderers; 2 | 3 | namespace LightTube.CustomRendererDatas; 4 | 5 | public class EditablePlaylistVideoRendererData : PlaylistVideoRendererData 6 | { 7 | public bool Editable { get; set; } 8 | } -------------------------------------------------------------------------------- /LightTube/CustomRendererDatas/SubscriptionFeedVideoRendererData.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Renderers; 2 | 3 | namespace LightTube.CustomRendererDatas; 4 | 5 | public class SubscriptionFeedVideoRendererData : PlaylistVideoRendererData 6 | { 7 | public DateTimeOffset ExactPublishDate { get; set; } 8 | } -------------------------------------------------------------------------------- /LightTube/Database/CacheManager.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Text; 3 | using LightTube.Database.Models; 4 | using MongoDB.Driver; 5 | 6 | namespace LightTube.Database; 7 | 8 | public class CacheManager(IMongoCollection channelCollection, 9 | IMongoCollection videoCollection) 10 | { 11 | public IMongoCollection ChannelCollection = channelCollection; 12 | public IMongoCollection VideoCollection = videoCollection; 13 | 14 | public async Task AddChannel(DatabaseChannel channel, bool updateOnly = false) 15 | { 16 | if (await ChannelCollection.CountDocumentsAsync(x => x.ChannelId == channel.ChannelId) > 0) 17 | await ChannelCollection.ReplaceOneAsync(x => x.ChannelId == channel.ChannelId, channel); 18 | else if (!updateOnly) 19 | await ChannelCollection.InsertOneAsync(channel); 20 | } 21 | 22 | public DatabaseChannel? GetChannel(string id) => 23 | ChannelCollection.FindSync(x => x.ChannelId == id).FirstOrDefault(); 24 | 25 | public async Task AddVideo(DatabaseVideo video, bool updateOnly = false) 26 | { 27 | if (await VideoCollection.CountDocumentsAsync(x => x.Id == video.Id) > 0) 28 | await VideoCollection.ReplaceOneAsync(x => x.Id == video.Id, video); 29 | else if (!updateOnly) 30 | await VideoCollection.InsertOneAsync(video); 31 | } 32 | 33 | public DatabaseVideo? GetVideo(string id) => 34 | VideoCollection.FindSync(x => x.Id == id).FirstOrDefault(); 35 | } -------------------------------------------------------------------------------- /LightTube/Database/DatabaseManager.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Chores; 2 | using LightTube.Database.Models; 3 | using LightTube.Database.Serialization; 4 | using MongoDB.Bson.Serialization; 5 | using MongoDB.Driver; 6 | 7 | namespace LightTube.Database; 8 | 9 | public static class DatabaseManager 10 | { 11 | public static IMongoDatabase Database { get; private set; } 12 | public static IMongoCollection UserCollection { get; private set; } 13 | public static IMongoCollection TokensCollection { get; private set; } 14 | public static IMongoCollection Oauth2TokensCollection { get; private set; } 15 | public static IMongoCollection VideoCacheCollection { get; private set; } 16 | public static IMongoCollection PlaylistCollection { get; private set; } 17 | public static IMongoCollection ChannelCacheCollection { get; private set; } 18 | public static UserManager Users { get; private set; } 19 | public static CacheManager Cache { get; private set; } 20 | public static Oauth2Manager Oauth2 { get; private set; } 21 | public static PlaylistManager Playlists { get; private set; } 22 | 23 | public static void Init(string connstr) 24 | { 25 | MongoClient client = new(connstr); 26 | Database = client.GetDatabase(Configuration.Database); 27 | UserCollection = Database.GetCollection("users"); 28 | TokensCollection = Database.GetCollection("tokens"); 29 | VideoCacheCollection = Database.GetCollection("videoCache"); 30 | PlaylistCollection = Database.GetCollection("playlists"); 31 | ChannelCacheCollection = Database.GetCollection("channelCache"); 32 | Oauth2TokensCollection = Database.GetCollection("oauth2Tokens"); 33 | 34 | Users = new UserManager(UserCollection, TokensCollection, PlaylistCollection, Oauth2TokensCollection); 35 | Cache = new CacheManager(ChannelCacheCollection, VideoCacheCollection); 36 | Oauth2 = new Oauth2Manager(Oauth2TokensCollection); 37 | Playlists = new PlaylistManager(PlaylistCollection, VideoCacheCollection); 38 | 39 | BsonSerializer.RegisterSerializationProvider(new LightTubeBsonSerializationProvider()); 40 | 41 | ChoreManager.QueueChore("DatabaseCleanup"); 42 | } 43 | } -------------------------------------------------------------------------------- /LightTube/Database/Models/DatabaseChannel.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using InnerTube.Models; 3 | using MongoDB.Bson.Serialization.Attributes; 4 | 5 | namespace LightTube.Database.Models; 6 | 7 | [BsonIgnoreExtraElements] 8 | public class DatabaseChannel 9 | { 10 | public string ChannelId; 11 | public string Name; 12 | public string Subscribers; 13 | public string IconUrl; 14 | 15 | public DatabaseChannel() 16 | { 17 | 18 | } 19 | 20 | public DatabaseChannel(InnerTubeChannel channel) 21 | { 22 | ChannelId = channel.Header!.Id; 23 | Name = channel.Header!.Title; 24 | Subscribers = channel.Header!.SubscriberCountText; 25 | IconUrl = channel.Header!.Avatars.Last().Url; 26 | } 27 | } -------------------------------------------------------------------------------- /LightTube/Database/Models/DatabaseLogin.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization.Attributes; 2 | using Newtonsoft.Json; 3 | 4 | namespace LightTube.Database.Models; 5 | 6 | [BsonIgnoreExtraElements] 7 | public class DatabaseLogin 8 | { 9 | public string Id; 10 | public string UserID; 11 | public string Token; 12 | [JsonIgnore] public string UserAgent; 13 | public string[] Scopes; 14 | [JsonIgnore] public DateTimeOffset Created = DateTimeOffset.MinValue; 15 | [JsonIgnore] public DateTimeOffset LastSeen = DateTimeOffset.MinValue; 16 | } -------------------------------------------------------------------------------- /LightTube/Database/Models/DatabaseOauthToken.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization.Attributes; 2 | 3 | namespace LightTube.Database.Models; 4 | 5 | [BsonIgnoreExtraElements] 6 | public class DatabaseOauthToken 7 | { 8 | public string UserId; 9 | public string? ClientId; 10 | public string RefreshToken; 11 | public string CurrentAuthToken; 12 | public string[] Scopes; 13 | public DateTimeOffset CurrentTokenExpirationDate; 14 | } -------------------------------------------------------------------------------- /LightTube/Database/Models/DatabaseUser.cs: -------------------------------------------------------------------------------- 1 | using InnerTube.Protobuf; 2 | using InnerTube.Renderers; 3 | using LightTube.Localization; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | using Newtonsoft.Json; 6 | 7 | namespace LightTube.Database.Models; 8 | 9 | [BsonIgnoreExtraElements] 10 | public class DatabaseUser 11 | { 12 | private const string ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 13 | [JsonProperty("userId")] public string UserID { get; set; } 14 | [JsonIgnore] public string PasswordHash { get; set; } 15 | [JsonIgnore] public Dictionary Subscriptions { get; set; } 16 | [JsonProperty("ltChannelId")] public string LTChannelID { get; set; } 17 | 18 | public static DatabaseUser CreateUser(string userId, string password) => 19 | new() 20 | { 21 | UserID = userId, 22 | PasswordHash = BCrypt.Net.BCrypt.HashPassword(password), 23 | Subscriptions = [], 24 | LTChannelID = GetChannelId(userId) 25 | }; 26 | 27 | public static string GetChannelId(string userId) 28 | { 29 | Random rng = new(userId.GetHashCode()); 30 | string channelId = "LT_UC"; 31 | while (channelId.Length < 24) 32 | channelId += ID_ALPHABET[rng.Next(0, ID_ALPHABET.Length)]; 33 | return channelId; 34 | } 35 | 36 | public List PlaylistRenderers(LocalizationManager localization, PlaylistVisibility minVisibility = PlaylistVisibility.Visible) 37 | { 38 | DatabasePlaylist[] playlists = 39 | DatabaseManager.Playlists.GetUserPlaylists(UserID, minVisibility).ToArray(); 40 | if (playlists.Length == 0) 41 | { 42 | return 43 | [ 44 | new RendererContainer 45 | { 46 | Type = "message", 47 | OriginalType = "messageRenderer", 48 | Data = new MessageRendererData(localization.GetRawString("channel.noplaylists")) 49 | } 50 | ]; 51 | } 52 | 53 | return playlists.Select(x => new RendererContainer 54 | { 55 | Type = "playlist", 56 | OriginalType = "gridPlaylistRenderer", 57 | Data = new PlaylistRendererData 58 | { 59 | PlaylistId = x.Id, 60 | Thumbnails = [ 61 | new Thumbnail 62 | { 63 | Url = $"https://i.ytimg.com/vi/{x.VideoIds.FirstOrDefault()}/hqdefault.jpg", 64 | Width = 480, 65 | Height = 360 66 | } 67 | ], 68 | Title = x.Name, 69 | VideoCountText = string.Format(localization.GetRawString("playlist.videos.count"), x.VideoIds.Count), 70 | VideoCount = x.VideoIds.Count, 71 | SidebarThumbnails = [], 72 | Author = null 73 | } 74 | }).ToList(); 75 | } 76 | } -------------------------------------------------------------------------------- /LightTube/Database/Models/DatabaseVideo.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using InnerTube.Models; 3 | using InnerTube.Protobuf; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | 6 | namespace LightTube.Database.Models; 7 | 8 | [BsonIgnoreExtraElements] 9 | public class DatabaseVideo 10 | { 11 | public string Id; 12 | public string Title; 13 | public Thumbnail[] Thumbnails; 14 | public string? UploadedAt; 15 | public long Views; 16 | [BsonIgnore] public string ViewsCount => $"{Views:N0} views"; 17 | public DatabaseVideoAuthor Channel; 18 | public string Duration; 19 | 20 | public DatabaseVideo() 21 | { 22 | } 23 | 24 | public DatabaseVideo(InnerTubePlayer player) 25 | { 26 | Id = player.Details.Id; 27 | Title = player.Details.Title; 28 | Thumbnails = player.Details.Thumbnails.ToArray(); 29 | UploadedAt = ""; 30 | Views = 0; 31 | Channel = new() 32 | { 33 | Id = player.Details.Author.Id, 34 | Name = player.Details.Author.Title 35 | }; 36 | Duration = player.Details.Length!.Value.ToDurationString(); 37 | } 38 | } -------------------------------------------------------------------------------- /LightTube/Database/Models/DatabaseVideoAuthor.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using InnerTube.Protobuf; 3 | using MongoDB.Bson.Serialization.Attributes; 4 | 5 | namespace LightTube.Database.Models; 6 | 7 | [BsonIgnoreExtraElements] 8 | public class DatabaseVideoAuthor 9 | { 10 | public string Id; 11 | public string Name; 12 | } -------------------------------------------------------------------------------- /LightTube/Database/Models/SubscriptionType.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.Database.Models; 2 | 3 | public enum SubscriptionType 4 | { 5 | NONE, 6 | NOTIFICATIONS_OFF, 7 | NOTIFICATIONS_ON 8 | } -------------------------------------------------------------------------------- /LightTube/Database/Oauth2Manager.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Database.Models; 2 | using Microsoft.Extensions.Primitives; 3 | using MongoDB.Driver; 4 | 5 | namespace LightTube.Database; 6 | 7 | public class Oauth2Manager(IMongoCollection oauthTokensCollection) 8 | { 9 | public IMongoCollection OauthTokensCollection { get; } = oauthTokensCollection; 10 | 11 | public async Task CreateOauthToken(string loginToken, string clientId, string[] scopes) 12 | { 13 | DatabaseUser user = (await DatabaseManager.Users.GetUserFromToken(loginToken))!; 14 | string refreshToken = Utils.GenerateToken(512); 15 | await OauthTokensCollection.InsertOneAsync(new DatabaseOauthToken 16 | { 17 | UserId = user.UserID, 18 | ClientId = clientId, 19 | RefreshToken = refreshToken, 20 | CurrentAuthToken = null, 21 | CurrentTokenExpirationDate = DateTimeOffset.UnixEpoch, 22 | Scopes = scopes 23 | }); 24 | return refreshToken; 25 | } 26 | 27 | public async Task RefreshToken(string refreshToken, string clientId) 28 | { 29 | await Task.Delay(1000); 30 | // returns null sometimes :sob: 31 | DatabaseOauthToken? token = 32 | (await OauthTokensCollection 33 | .FindAsync(x => x.RefreshToken == refreshToken)) 34 | .FirstOrDefault(); 35 | if (token is null) return null; 36 | token.CurrentAuthToken = Utils.GenerateToken(256); 37 | token.CurrentTokenExpirationDate = DateTimeOffset.Now.AddHours(1); 38 | await OauthTokensCollection.FindOneAndReplaceAsync(x => x.RefreshToken == refreshToken, token); 39 | return token; 40 | } 41 | 42 | public async Task GetUserFromHeader(string authHeader) 43 | { 44 | string[] parts = authHeader.Split(" "); 45 | string type = parts[0].ToLower(); 46 | string token = parts[1]; 47 | 48 | if (type != "bearer") return null; 49 | 50 | IAsyncCursor cursor = 51 | await OauthTokensCollection.FindAsync(x => x.CurrentAuthToken == token); 52 | DatabaseOauthToken login = cursor.FirstOrDefault(); 53 | 54 | if (login is null) 55 | return null; 56 | 57 | return await DatabaseManager.Users.GetUserFromId(login.UserId); 58 | } 59 | 60 | public async Task GetLoginFromHttpContext(HttpContext context) 61 | { 62 | if (!context.Request.Headers.TryGetValue("authorization", out StringValues authHeaders)) 63 | return null; 64 | 65 | string[] parts = authHeaders.First().Split(" "); 66 | string type = parts[0].ToLower(); 67 | string token = parts[1]; 68 | 69 | if (type != "bearer") return null; 70 | 71 | IAsyncCursor cursor = 72 | await OauthTokensCollection.FindAsync(x => x.CurrentAuthToken == token); 73 | DatabaseOauthToken login = cursor.FirstOrDefault(); 74 | 75 | return login; 76 | } 77 | 78 | public async Task GetUserFromHttpRequest(HttpRequest request) 79 | { 80 | if (!request.Headers.TryGetValue("authorization", out StringValues authHeaders)) 81 | return null; 82 | 83 | string authHeader = authHeaders.First(); 84 | return await DatabaseManager.Oauth2.GetUserFromHeader(authHeader); 85 | } 86 | } -------------------------------------------------------------------------------- /LightTube/Database/Serialization/BsonNullableIntSerializer.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson; 2 | using MongoDB.Bson.Serialization; 3 | using MongoDB.Bson.Serialization.Serializers; 4 | 5 | namespace LightTube.Database.Serialization; 6 | 7 | public class BsonNullableIntSerializer : SerializerBase 8 | { 9 | public override int Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) 10 | { 11 | BsonType type = context.Reader.GetCurrentBsonType(); 12 | switch (type) 13 | { 14 | case BsonType.Null: 15 | context.Reader.ReadNull(); 16 | return 0; 17 | case BsonType.Int32: 18 | return context.Reader.ReadInt32(); 19 | default: 20 | throw new NotSupportedException($"Cannot convert a {type} to a Int32."); 21 | } 22 | } 23 | 24 | public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, int value) 25 | { 26 | context.Writer.WriteInt32(value); 27 | } 28 | } -------------------------------------------------------------------------------- /LightTube/Database/Serialization/BsonNullableStringSerializer.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson; 2 | using MongoDB.Bson.Serialization; 3 | using MongoDB.Bson.Serialization.Serializers; 4 | 5 | namespace LightTube.Database.Serialization; 6 | 7 | public class BsonNullableStringSerializer : SerializerBase 8 | { 9 | public override string Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) 10 | { 11 | BsonType type = context.Reader.GetCurrentBsonType(); 12 | switch (type) 13 | { 14 | case BsonType.Null: 15 | context.Reader.ReadNull(); 16 | return ""; 17 | case BsonType.String: 18 | return context.Reader.ReadString(); 19 | default: 20 | return ""; 21 | } 22 | } 23 | 24 | public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string value) 25 | { 26 | context.Writer.WriteString(value ?? ""); 27 | } 28 | } -------------------------------------------------------------------------------- /LightTube/Database/Serialization/LightTubeBsonSerializationProvider.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization; 2 | 3 | namespace LightTube.Database.Serialization; 4 | 5 | public class LightTubeBsonSerializationProvider : IBsonSerializationProvider 6 | { 7 | public IBsonSerializer? GetSerializer(Type type) 8 | { 9 | return type == typeof(int) ? new BsonNullableIntSerializer() : 10 | type == typeof(string) ? new BsonNullableStringSerializer() : null; 11 | } 12 | } -------------------------------------------------------------------------------- /LightTube/DropdownItem.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube; 2 | 3 | public class DropdownItem(string label, string target, string icon) 4 | { 5 | public string Label = label; 6 | public string Target = target; 7 | public string Icon = icon; 8 | } -------------------------------------------------------------------------------- /LightTube/GuideItem.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube; 2 | 3 | public class GuideItem(string title, string iconId, string path, bool visibleOnMinifiedGuide) 4 | { 5 | public string Title = title; 6 | public string IconId = iconId; 7 | public string Path = path; 8 | public bool VisibleOnMinifiedGuide = visibleOnMinifiedGuide; 9 | } -------------------------------------------------------------------------------- /LightTube/Health/HealthManager.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.Health; 2 | 3 | public static class HealthManager 4 | { 5 | private static List> videoStatuses = []; 6 | private static List playerResponseTimes = []; 7 | 8 | public static void PushVideoResponse(string videoId, bool isSuccess, long playerResponseTime) 9 | { 10 | // don't include cache hits 11 | if (playerResponseTime < 50) return; 12 | 13 | // if entry with the same videoId exists, remove it 14 | videoStatuses.RemoveAll(x => x.Key == videoId); 15 | 16 | // only keep last 100 requests 17 | if (videoStatuses.Count >= 100) videoStatuses.RemoveAt(0); 18 | if (playerResponseTimes.Count >= 100) playerResponseTimes.RemoveAt(0); 19 | 20 | playerResponseTimes.Add(playerResponseTime); 21 | videoStatuses.Add(new KeyValuePair(videoId, isSuccess)); 22 | } 23 | 24 | public static float GetHealthPercentage() => 25 | Math.Clamp(MathF.Round((float)videoStatuses.Count(x => x.Value) / Math.Max(videoStatuses.Count, 1) * 100), 0, 100); 26 | 27 | public static double GetAveragePlayerResponseTime() 28 | { 29 | if (playerResponseTimes.Count == 0) return 0; 30 | return playerResponseTimes.Average(); 31 | } 32 | } -------------------------------------------------------------------------------- /LightTube/Importer/ImportedData.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using LightTube.Database.Models; 3 | using Newtonsoft.Json; 4 | 5 | namespace LightTube; 6 | 7 | public class ImportedData(ImportSource source) 8 | { 9 | public ImportSource Source = source; 10 | public List Subscriptions = []; 11 | public List Playlists = []; 12 | 13 | public override string ToString() 14 | { 15 | StringBuilder sb = new(); 16 | sb.AppendLine("=== EXPORTED DATA ===") 17 | .AppendLine("Source: " + Source) 18 | .AppendLine() 19 | .AppendLine("=== Channels"); 20 | foreach (Subscription s in Subscriptions) 21 | sb.AppendLine(s.Id + ": " + (s.Name ?? "")); 22 | 23 | sb.AppendLine() 24 | .AppendLine("=== Playlists"); 25 | 26 | foreach (Playlist p in Playlists) 27 | { 28 | sb.AppendLine("Title: " + p.Title) 29 | .AppendLine("Description: " + p.Description) 30 | .AppendLine("TimeCreated: " + (p.TimeCreated?.ToString() ?? "")) 31 | .AppendLine("TimeUpdated: " + (p.TimeUpdated?.ToString() ?? "")) 32 | .AppendLine("Visibility: " + p.Visibility) 33 | .AppendLine(string.Join("\n", p.VideoIds.Select(x => $"- {x}"))); 34 | sb.AppendLine("==="); 35 | } 36 | 37 | return sb.ToString(); 38 | } 39 | 40 | public class Subscription 41 | { 42 | public string Id; 43 | public string? Name; 44 | } 45 | 46 | public class Playlist 47 | { 48 | [JsonProperty("title")] public string Title; 49 | [JsonProperty("description")] public string Description; 50 | [JsonProperty("created")] public DateTimeOffset? TimeCreated; 51 | [JsonProperty("updated")] public DateTimeOffset? TimeUpdated; 52 | [JsonProperty("visibility")] public PlaylistVisibility Visibility; 53 | [JsonProperty("videos")] public string[] VideoIds; 54 | } 55 | } -------------------------------------------------------------------------------- /LightTube/Importer/LightTubeExport.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace LightTube; 4 | 5 | public class LightTubeExport 6 | { 7 | [JsonProperty("type")] public string Type { get; set; } 8 | [JsonProperty("host")] public string Host { get; set; } 9 | [JsonProperty("subscriptions")] public string[] Subscriptions { get; set; } 10 | [JsonProperty("playlists")] public ImportedData.Playlist[] Playlists { get; set; } 11 | } -------------------------------------------------------------------------------- /LightTube/JsCache.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using Serilog; 3 | 4 | namespace LightTube; 5 | 6 | public static class JsCache 7 | { 8 | private static Dictionary LibraryUrls = new() 9 | { 10 | ["hls.js"] = new Uri("https://cdn.jsdelivr.net/npm/hls.js@1.4.0/dist/hls.min.js"), 11 | ["ltplayer.js"] = new Uri("https://raw.githubusercontent.com/lighttube-org/LTPlayer/master/dist/player.min.js"), 12 | ["ltplayer.css"] = new Uri("https://raw.githubusercontent.com/lighttube-org/LTPlayer/master/dist/player.min.css"), 13 | }; 14 | private static Dictionary Hashes = []; 15 | public static DateTimeOffset CacheUpdateTime = DateTimeOffset.MinValue; 16 | 17 | public static async Task DownloadLibraries() 18 | { 19 | HttpClient client = new(); 20 | Directory.CreateDirectory("/tmp/lighttube/jsCache"); 21 | Log.Information("[JsCache] Downloading libraries..."); 22 | foreach ((string? name, Uri? url) in LibraryUrls) 23 | { 24 | Log.Information($"[JsCache] Downloading '{name}' from {url}"); 25 | 26 | HttpResponseMessage response = await client.GetAsync(url); 27 | string jsData = await response.Content.ReadAsStringAsync(); 28 | await File.WriteAllTextAsync($"/tmp/lighttube/jsCache/{name}", jsData); 29 | Log.Debug($"[JsCache] Calculating the MD5 hash of {name}..."); 30 | 31 | Hashes[name] = Utils.Md5Sum(jsData); 32 | 33 | Log.Information($"[JsCache] Downloaded '{name}'."); 34 | } 35 | CacheUpdateTime = DateTimeOffset.UtcNow; 36 | } 37 | 38 | public static string GetJsFileContents(string name) => File.ReadAllText($"/tmp/lighttube/jsCache/{HttpUtility.UrlEncode(name)}"); 39 | public static Uri GetUrl(string name) => LibraryUrls.TryGetValue(name, out Uri? url) ? url : new Uri("/"); 40 | 41 | public static string GetHash(string name) => 42 | Hashes.TryGetValue(name, out string? h) ? h : "68b329da9893e34099c7d8ad5cb9c940"; // md5 sum of an empty buffer 43 | } -------------------------------------------------------------------------------- /LightTube/LightTube.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LightTube/Localization/Language.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace LightTube.Localization; 4 | 5 | public class Language 6 | { 7 | public string Code { get; set; } 8 | public string Name { get; set; } 9 | public string EnglishName { get; set; } 10 | public CultureInfo Culture { get; set; } 11 | 12 | public string GetDisplayName() => Name == EnglishName ? Name : $"{Name} - {EnglishName}"; 13 | } -------------------------------------------------------------------------------- /LightTube/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.Models; 2 | 3 | public class ErrorViewModel 4 | { 5 | public string? RequestId { get; set; } 6 | 7 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 8 | } -------------------------------------------------------------------------------- /LightTube/PoToken/PoTokenData.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.PoToken; 2 | 3 | public class PoTokenData 4 | { 5 | public string VisitorData { get; set; } 6 | public string PoToken { get; set; } 7 | } -------------------------------------------------------------------------------- /LightTube/PoToken/PoTokenManager.cs: -------------------------------------------------------------------------------- 1 | using InnerTube; 2 | using Serilog; 3 | 4 | namespace LightTube.PoToken; 5 | 6 | public static class PoTokenManager 7 | { 8 | private static Timer refreshTimer; 9 | private static SimpleInnerTubeClient innerTube; 10 | private static HttpClient httpClient = new(); 11 | private static string apiUrl = ""; 12 | 13 | private static List requiredClients = 14 | [ 15 | RequestClient.WEB, 16 | RequestClient.TV_EMBEDDED 17 | ]; 18 | 19 | public static void Init(string apiUrl, SimpleInnerTubeClient innerTubeClient) 20 | { 21 | Log.Information("[PoTokenManager] Initializing PoToken manager..."); 22 | PoTokenManager.apiUrl = apiUrl; 23 | innerTube = innerTubeClient; 24 | refreshTimer = new Timer(RefreshPoToken, null, TimeSpan.Zero, TimeSpan.FromMinutes(15)); 25 | } 26 | 27 | private static async void RefreshPoToken(object? _) 28 | { 29 | foreach (RequestClient client in requiredClients) 30 | { 31 | try 32 | { 33 | PoTokenResponse? response = await httpClient.GetFromJsonAsync($"{apiUrl}/generate"); 34 | 35 | if (response == null) throw new Exception("Response is null"); 36 | if (!response.Success) throw new Exception(response.Error); 37 | if (response.Response == null) throw new Exception("response.Response is null? what???"); 38 | 39 | innerTube.ProvideSecrets(client, response.Response.VisitorData, response.Response.PoToken); 40 | Log.Information("[PoTokenManager] Loaded secrets for client {0}\nVisitorInfo: {1}\nPoToken: {2}", client, 41 | response.Response.VisitorData, response.Response.PoToken); 42 | } 43 | catch (Exception e) 44 | { 45 | Log.Error(e, "[PoTokenManager] Failed to get PoToken for client {0}.", client); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /LightTube/PoToken/PoTokenResponse.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube.PoToken; 2 | 3 | public class PoTokenResponse 4 | { 5 | public bool Success { get; set; } 6 | public PoTokenData? Response { get; set; } 7 | public string? Error { get; set; } 8 | } -------------------------------------------------------------------------------- /LightTube/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using InnerTube; 3 | using LightTube; 4 | using LightTube.Chores; 5 | using LightTube.Database; 6 | using LightTube.Localization; 7 | using LightTube.PoToken; 8 | using Newtonsoft.Json.Converters; 9 | using Newtonsoft.Json.Serialization; 10 | using Serilog; 11 | using Serilog.Events; 12 | 13 | 14 | Log.Logger = new LoggerConfiguration() 15 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information) 16 | .Enrich.FromLogContext() 17 | .WriteTo.Console() 18 | .CreateBootstrapLogger(); 19 | 20 | Configuration.InitConfig(); 21 | LocalizationManager.Init(); 22 | try 23 | { 24 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 25 | 26 | builder.Host.UseSerilog((context, services, configuration) => configuration 27 | .ReadFrom.Configuration(context.Configuration) 28 | .ReadFrom.Services(services) 29 | .Enrich.FromLogContext() 30 | .WriteTo.Console()); 31 | 32 | // Add services to the container. 33 | builder.Services 34 | .AddControllersWithViews() 35 | .AddNewtonsoftJson(options => 36 | { 37 | options.SerializerSettings.Converters.Add(new StringEnumConverter(new DefaultNamingStrategy(), false)); 38 | }); 39 | 40 | InnerTubeAuthorization? auth = Configuration.InnerTubeAuthorization; 41 | SimpleInnerTubeClient innerTube = new SimpleInnerTubeClient(new InnerTubeConfiguration 42 | { 43 | Authorization = auth, 44 | CacheSize = Configuration.CacheSize, 45 | CacheExpirationPollingInterval = default 46 | }); 47 | if (Configuration.PoTokenGeneratorUrl != null) 48 | PoTokenManager.Init(Configuration.PoTokenGeneratorUrl, innerTube); 49 | builder.Services.AddSingleton(innerTube); 50 | builder.Services.AddSingleton(new HttpClient()); 51 | 52 | await JsCache.DownloadLibraries(); 53 | ChoreManager.RegisterChores(); 54 | DatabaseManager.Init(Configuration.ConnectionString); 55 | 56 | WebApplication app = builder.Build(); 57 | 58 | // Configure the HTTP request pipeline. 59 | if (!app.Environment.IsDevelopment()) 60 | { 61 | app.UseExceptionHandler("/Home/Error"); 62 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 63 | app.UseHsts(); 64 | app.UseHttpsRedirection(); 65 | } 66 | 67 | app.UseStaticFiles(new StaticFileOptions 68 | { 69 | OnPrepareResponse = ctx => 70 | { 71 | // Cache static files for 3 days 72 | ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=259200"); 73 | ctx.Context.Response.Headers.Append("Expires", 74 | DateTime.UtcNow.AddDays(3).ToString("R", CultureInfo.InvariantCulture)); 75 | } 76 | }); 77 | 78 | app.UseRouting(); 79 | 80 | app.UseAuthorization(); 81 | 82 | app.MapControllerRoute( 83 | name: "default", 84 | pattern: "{controller=Home}/{action=Index}/{id?}"); 85 | 86 | app.Run(); 87 | } 88 | catch (Exception ex) 89 | { 90 | Log.Fatal(ex, "Application terminated unexpectedly"); 91 | } 92 | finally 93 | { 94 | Log.CloseAndFlush(); 95 | } -------------------------------------------------------------------------------- /LightTube/Resources/Localization/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "language.name.english": "Indonesian", 3 | "language.code": "id", 4 | "language.name": "Inggris", 5 | "language.innertube": "id", 6 | "language.ietf": "id" 7 | } 8 | -------------------------------------------------------------------------------- /LightTube/Resources/Localization/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "language.code": "pt", 3 | "language.name": "Português", 4 | "language.name.english": "Portuguese", 5 | "language.ietf": "pt", 6 | "language.innertube": "pt-PT", 7 | "titles.account.register": "Criar uma conta", 8 | "titles.account.login": "Fazer o Login", 9 | "titles.account.delete": "Excluir a sua conta", 10 | "account.userid": "ID do Usuário", 11 | "account.password": "Senha", 12 | "account.rememberme": "Lembre-se de mim", 13 | "account.confirmpassword": "Confirmar Senha", 14 | "account.login": "Fazer o Login", 15 | "account.signup": "Cadastrar", 16 | "account.register": "Criar uma conta", 17 | "account.delete": "Excluir", 18 | "account.register.disabled": "Essa instância não aceita registrar conta", 19 | "channel.tab.featured": "Home", 20 | "channel.tab.videos": "Vídeos", 21 | "channel.tab.shorts": "Shorts", 22 | "channel.tab.streams": "Ao vivo", 23 | "channel.tab.playlists": "Lista de Reprodução", 24 | "channel.tab.podcasts": "Podcasts", 25 | "channel.tab.releases": "Lançamentos", 26 | "channel.tab.community": "Comunidade", 27 | "channel.tab.channels": "Canais", 28 | "channel.tab.store": "Loja", 29 | "channel.tab.about": "Sobre", 30 | "channel.tab.search": "Pesquisar", 31 | "channel.tagline.lighttube": "Canal do LightTube", 32 | "channel.noplaylists": "Esse usuário não possui nenhuma lista pública.", 33 | "download.title": "Baixar vídeo", 34 | "download.format.select": "Por favor escolha um formato para baixar.", 35 | "download.format.video": "Somente vídeo", 36 | "download.format.audio": "Somente áudio", 37 | "download.proxy": "Baixar com proxy", 38 | "feed.library.title": "Biblioteca", 39 | "layout.dropdown.login": "Fazer o login", 40 | "layout.dropdown.logout": "Desconectar", 41 | "layout.guide.home": "Home", 42 | "layout.guide.subscriptions": "Inscrições", 43 | "layout.guide.library": "Biblioteca", 44 | "feed.subscriptions.title": "Todas as Inscrições", 45 | "feed.subscriptions.manage": "Administrar Inscrições", 46 | "account.showpassword": "Mostrar senha", 47 | "account.password.confirm": "Confirmar Senha", 48 | "channel.about.artistbio": "Biografia do Artista", 49 | "channel.banner": "Banner do Canal", 50 | "account.deleteconfirm": "Eu entendo que clicando, todas as minhas listas, inscrições de canais, etc. serão excluídas dessa instancia do LightTube. ({0})", 51 | "channel.about.description": "Descrição", 52 | "channel.about.views": "{0} visualizações", 53 | "channel.supporter.avatar": "Avatar do Usuário", 54 | "channel.accountsettings": "Configurações da conta", 55 | "channel.header.subscribers": "{0} inscritos", 56 | "channel.recognition.title": "Nossos Membros", 57 | "channel.header.videos": "{0} vídeos", 58 | "feed.library.playlist.new": "Nova Lista de Reprodução", 59 | "download.direct": "Baixar direto", 60 | "feed.title": "Inscrições", 61 | "feed.subscriptions.apply": "Aplicar Mudanças", 62 | "layout.region": "Região do Conteúdo do InnerTube", 63 | "layout.dropdown.user": "Fazer o login como: {0}", 64 | "layout.dropdown.settings": "Configurações", 65 | "layout.dropdown.about": "Sobre" 66 | } 67 | -------------------------------------------------------------------------------- /LightTube/Resources/Localization/ro.json: -------------------------------------------------------------------------------- 1 | { 2 | "language.code": "ro", 3 | "language.name": "Română", 4 | "language.name.english": "Romanian", 5 | "language.ietf": "ro", 6 | "language.innertube": "ro", 7 | "titles.account.register": "Creează un cont", 8 | "titles.account.login": "Logează-te", 9 | "titles.account.delete": "Ștergeți contul", 10 | "account.userid": "Nume de utilizator", 11 | "account.showpassword": "Arată parola", 12 | "account.rememberme": "Amintește-mi", 13 | "account.login": "Logare", 14 | "account.register": "Creează cont", 15 | "account.delete": "Șterge", 16 | "account.password": "Parolă", 17 | "account.password.confirm": "Confirmă parola", 18 | "account.confirmpassword": "Confirmă parola", 19 | "account.signup": "Înregistrează-te", 20 | "account.deleteconfirm": "Înțeleg că apăsând Șterge, tot playlist-urile mele, canale abonate etc. vor fi șterse de pe această instanță LightTube ({0})" 21 | } 22 | -------------------------------------------------------------------------------- /LightTube/Resources/Localization/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "language.code": "ru", 3 | "language.ietf": "ru", 4 | "language.innertube": "ru", 5 | "titles.account.register": "Создать аккаунт", 6 | "titles.account.login": "Войти", 7 | "titles.account.delete": "Удалить ваш аккаунт", 8 | "account.userid": "UserID", 9 | "account.password": "Пароль", 10 | "language.name.english": "Russian", 11 | "language.name": "Русский" 12 | } 13 | -------------------------------------------------------------------------------- /LightTube/Resources/Localization/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "language.innertube": "tr", 3 | "titles.account.register": "Hesap Oluştur", 4 | "titles.account.login": "Giriş yap", 5 | "titles.account.delete": "Hesabını sil", 6 | "account.userid": "UserID", 7 | "account.password": "Parola", 8 | "account.password.confirm": "Parolayı Doğrula", 9 | "account.showpassword": "Parolayı göster", 10 | "account.rememberme": "Beni hatırla", 11 | "account.confirmpassword": "Parolayı Doğrula", 12 | "account.login": "Giriş yap", 13 | "account.register": "Hesap oluştur", 14 | "account.logininstead": "Bunun yerine oturum açın", 15 | "account.delete": "Sil", 16 | "account.register.disabled": "Bu sunucu, hesap kayıtlarına izin vermiyor", 17 | "account.register.disabled.home": "Anasayfa'ya dön", 18 | "channel.about.artistbio": "Sanatçı", 19 | "channel.about.views": "{0} izlenme", 20 | "language.name": "Türkçe", 21 | "language.name.english": "Turkish", 22 | "language.code": "tr", 23 | "language.ietf": "tr", 24 | "account.signup": "Kayıt ol", 25 | "account.deleteconfirm": "Sil'e tıklayarak, tüm listelerim, abone kanallarım vs. bu LightTube sunucusun'dan silineceğini anlıyorum ({0})", 26 | "channel.about.description": "Açıklama" 27 | } 28 | -------------------------------------------------------------------------------- /LightTube/Resources/Localization/vi.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LightTube/Resources/Localization/zh_Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "language.name": "中文(简体)", 3 | "titles.account.register": "创建账号", 4 | "titles.account.login": "登录", 5 | "account.userid": "用户ID", 6 | "account.password": "密码", 7 | "account.password.confirm": "重复密码", 8 | "account.showpassword": "显示密码", 9 | "account.confirmpassword": "重复密码", 10 | "account.signup": "注册", 11 | "account.register": "创建账号", 12 | "account.logininstead": "直接登录", 13 | "account.delete": "注销", 14 | "account.register.disabled": "此LightTube实例不允许注册账号", 15 | "account.register.disabled.home": "返回主页", 16 | "channel.about.description": "简介", 17 | "channel.about.artistbio": "艺术家简介", 18 | "channel.about.views": "{0} 播放", 19 | "channel.about.links": "链接", 20 | "channel.banner": "频道横幅", 21 | "language.code": "zh-Hans", 22 | "language.name.english": "Chinese (Simplified)", 23 | "titles.account.delete": "注销账户", 24 | "channel.accountsettings": "账号设置", 25 | "account.rememberme": "记住密码", 26 | "account.login": "登录", 27 | "account.deleteconfirm": "我理解点击注销后,我在这个LightTube实例上的所有播放列表、关注的频道等将被删除 ({0})", 28 | "channel.supporter.avatar": "用户头像", 29 | "language.ietf": "zh", 30 | "language.innertube": "zh-CN" 31 | } 32 | -------------------------------------------------------------------------------- /LightTube/SearchAutocomplete.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Web; 3 | 4 | namespace LightTube; 5 | 6 | public class SearchAutocomplete(string query, string[] autocomplete) 7 | { 8 | private static HttpClient client = new(); 9 | public string Query { get; } = query; 10 | public string[] Autocomplete { get; } = autocomplete; 11 | 12 | public static async Task GetAsync(string query, string language = "en", string region = "us") 13 | { 14 | string url = "https://suggestqueries-clients6.youtube.com/complete/search?client=firefox&ds=yt" + 15 | $"&hl={HttpUtility.UrlEncode(language.ToLower())}" + 16 | $"&gl={HttpUtility.UrlEncode(region.ToLower())}" + 17 | $"&q={HttpUtility.UrlEncode(query)}"; 18 | string json = await client.GetStringAsync(url); 19 | object[] parsed = JsonSerializer.Deserialize(json)!; 20 | return new SearchAutocomplete(parsed[0] as string ?? query, parsed[1] as string[] ?? []); 21 | } 22 | } -------------------------------------------------------------------------------- /LightTube/SettingsTab.cs: -------------------------------------------------------------------------------- 1 | namespace LightTube; 2 | 3 | public class SettingsTab(string label, string path) 4 | { 5 | public string Label = label; 6 | public string Path = path; 7 | } -------------------------------------------------------------------------------- /LightTube/SponsorBlockSegment.cs: -------------------------------------------------------------------------------- 1 | using LightTube.Localization; 2 | using Newtonsoft.Json; 3 | 4 | namespace LightTube; 5 | 6 | public class SponsorBlockSegment 7 | { 8 | [JsonProperty("category")] public string Category { get; set; } 9 | [JsonProperty("actionType")] public string ActionType { get; set; } 10 | [JsonProperty("segment")] private double[] Segment { get; set; } 11 | [JsonProperty("UUID")] public string Uuid { get; set; } 12 | [JsonProperty("videoDuration")] public double VideoDuration { get; set; } 13 | [JsonProperty("locked")] public long Locked { get; set; } 14 | [JsonProperty("votes")] public long Votes { get; set; } 15 | [JsonProperty("description")] public string Description { get; set; } 16 | public double StartMs => Segment[0]; 17 | public double EndMs => Segment[1]; 18 | 19 | public string ToLTPlayerJson(double videoDuration, LocalizationManager localization) => 20 | $"{{ from: {ToPercentage(StartMs, videoDuration)}, to: {ToPercentage(EndMs, videoDuration)}, color: '#{GetColor()}', onEnter: function(player) {{ player.showSkipButton('{GetName(localization)}', {EndMs});}},onExit:function(player) {{player.hideSkipButton();}} }}"; 21 | 22 | private string GetName(LocalizationManager localization) => string.Format( 23 | localization.GetRawString("sponsorblock.button.template"), 24 | localization.GetRawString("sponsorblock.category." + Category)); 25 | 26 | private string GetColor() 27 | { 28 | return Category switch 29 | { 30 | "sponsor" => "00d400", 31 | "selfpromo" => "ff0", 32 | "interaction" => "cof", 33 | "intro" => "0ff", 34 | "outro" => "0202ed", 35 | "preview" => "008fd6", 36 | "filler" => "7300ff", 37 | _ => "#ff0" 38 | }; 39 | } 40 | 41 | private double ToPercentage(double input, double max) => (input / max) * 100; 42 | 43 | public static async Task GetSponsors(string videoId) 44 | { 45 | HttpResponseMessage sbResponse = 46 | await new HttpClient().GetAsync( 47 | $"https://sponsor.ajay.app/api/skipSegments?videoID={videoId}&category=sponsor&category=selfpromo&category=interaction&category=intro&category=outro&category=preview&category=music_offtopic&category=filler"); 48 | if (!sbResponse.IsSuccessStatusCode) return []; 49 | string json = await sbResponse.Content.ReadAsStringAsync(); 50 | return JsonConvert.DeserializeObject(json)!; 51 | } 52 | } -------------------------------------------------------------------------------- /LightTube/Views/Account/Delete.cshtml: -------------------------------------------------------------------------------- 1 | @model AccountContext 2 | 3 | @{ 4 | Layout = "_AccountLayout"; 5 | Model.HtmlTitle = Model.Localization.GetRawString("titles.account.delete"); 6 | } 7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 | 24 |
25 | 26 | 30 | 31 | -------------------------------------------------------------------------------- /LightTube/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model AccountContext 2 | 3 | @{ 4 | Layout = "_AccountLayout"; 5 | Model.HtmlTitle = Model.Localization.GetRawString("titles.account.login"); 6 | } 7 | 8 | 11 | 14 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /LightTube/Views/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @model AccountContext 2 | 3 | @{ 4 | Layout = "_AccountLayout"; 5 | Model.HtmlTitle = Model.Localization.GetRawString("titles.account.register"); 6 | } 7 | 8 | @if (!Configuration.RegistrationEnabled) 9 | { 10 |

@Model.Localization.GetString("account.register.disabled")

11 | 12 | 16 | } 17 | else 18 | { 19 | 22 | 23 | 26 | 27 | 30 | 31 | 35 | 39 | 40 | 44 | } 45 | -------------------------------------------------------------------------------- /LightTube/Views/Feed/AddToPlaylist.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Database.Models 2 | @model PlaylistVideoContext> 3 | 4 | @{ 5 | Layout = "_ModalLayout"; 6 | } 7 | 8 | 21 | 22 |

23 | @Model.Localization.GetString("playlist.add.body") 24 |

25 | 26 | 27 | -------------------------------------------------------------------------------- /LightTube/Views/Feed/Channels.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Database.Models 2 | @model ChannelsContext 3 | 4 | @{ 5 | Model.Title = Model.Localization.GetRawString("feed.subscriptions.title"); 6 | } 7 | 8 |
9 | 10 | 11 | @foreach (DatabaseChannel? channel in Model.Channels) 12 | { 13 | if (channel != null) { 14 |
15 | 16 | @channel.Name 17 | 18 |
19 | 20 | @channel.Name 21 | 22 |
23 | @channel.Subscribers 24 |
25 |
26 | 48 |
49 | } 50 | } 51 |
-------------------------------------------------------------------------------- /LightTube/Views/Feed/DeletePlaylist.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Database.Models 2 | @model PlaylistVideoContext> 3 | 4 | @{ 5 | Layout = "_ModalLayout"; 6 | } 7 | 8 | 21 | 22 |

23 | @Model.Localization.GetString("playlist.delete.body") 24 |

25 | 26 | -------------------------------------------------------------------------------- /LightTube/Views/Feed/EditPlaylist.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Database.Models 2 | @model PlaylistVideoContext> 3 | 4 | @{ 5 | Layout = "_ModalLayout"; 6 | } 7 | 8 |
9 |
10 |
11 | @Model.Localization.GetString("playlist.edit.label.title") 12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 | @Model.Localization.GetString("playlist.edit.label.description") 23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 | @Model.Localization.GetString("playlist.edit.label.visibility") 34 |
35 |
36 |
37 | 58 |
59 |
60 | 61 | -------------------------------------------------------------------------------- /LightTube/Views/Feed/Library.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Database.Models 2 | @model LibraryContext 3 | 4 | @{ 5 | Model.Title = Model.Localization.GetRawString("feed.library.title"); 6 | } 7 | 8 | -------------------------------------------------------------------------------- /LightTube/Views/Feed/NewPlaylist.cshtml: -------------------------------------------------------------------------------- 1 | @model ModalContext 2 | 3 | @{ 4 | Layout = "_ModalLayout"; 5 | } 6 | 7 |
8 |
9 |
10 | @Model.Localization.GetString("playlist.edit.label.title") 11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | @Model.Localization.GetString("playlist.edit.label.description") 22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | @Model.Localization.GetString("playlist.edit.label.visibility") 33 |
34 |
35 |
36 | 41 |
42 |
43 | 44 | -------------------------------------------------------------------------------- /LightTube/Views/Feed/RemoveFromPlaylist.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Database.Models 2 | @model PlaylistVideoContext> 3 | 4 | @{ 5 | Layout = "_ModalLayout"; 6 | } 7 | 8 | 21 | 22 |

23 | @Model.Localization.FormatString("playlist.removevideo.body", Model.Buttons[0].Label) 24 |

25 | 26 | 27 | -------------------------------------------------------------------------------- /LightTube/Views/Feed/Subscription.cshtml: -------------------------------------------------------------------------------- 1 | @using Humanizer 2 | @model SubscriptionsContext 3 | 4 | @{ 5 | Model.Title = Model.Localization.GetRawString("feed.title"); 6 | } 7 | 8 | 9 | 10 | @Model.Localization.GetString("feed.subscriptions.manage") 11 | 12 | 13 | 14 |
15 | @foreach (FeedVideo video in Model.Videos) 16 | { 17 |
18 | 19 | @video.Title 20 | 21 |
22 | 23 | @video.Title 24 | 25 | 30 |
31 | @Model.Localization.FormatString("video.subtitle", video.ViewCount.ToString("N0"), video.PublishedDate.Humanize(DateTimeOffset.UtcNow, Model.Localization.GetCulture())) 32 |
33 |
34 |
35 | } 36 |
-------------------------------------------------------------------------------- /LightTube/Views/Home/Error.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Localization 2 | @using Microsoft.AspNetCore.Diagnostics 3 | @{ 4 | Layout = null; 5 | IExceptionHandlerPathFeature error = Context.Features.Get()!; 6 | Exception exception = 7 | error.Error.InnerException ?? 8 | error.Error ?? 9 | new Exception("Nothing went wrong..?"); 10 | LocalizationManager localization = LocalizationManager.GetFromHttpContext(Context); 11 | 12 | bool innertubeError = exception.StackTrace?.Contains("InnerTube") ?? false; 13 | bool errorReportable = true; 14 | string message = "Something went wrong..."; 15 | string path = error.Path + Context.Request.QueryString; 16 | 17 | if (exception.GetType().Namespace == "InnerTube.Exceptions") 18 | { 19 | errorReportable = false; 20 | message = error.Error.Message; 21 | } 22 | } 23 | 24 | 25 | 26 | 27 | 28 | Error - LightTube 29 | 30 | 31 | 32 | 33 | @if (Configuration.CustomCssPath != null) 34 | { 35 | 36 | } 37 | 38 | 39 |

@localization.GetString("error.title")

40 |

@message

41 | 42 | @if (errorReportable) { 43 | @if (innertubeError) { 44 |

@localization.GetString("error.body.innertube")

45 | } else { 46 |

@localization.GetString("error.body.lighttube")

47 | } 48 | 49 |
    50 |
  • @localization.GetString("error.body.details.instance") (@Context.Request.Host)
  • 51 |
  • @localization.GetString("error.body.details.version") (@Utils.GetVersion() / @Utils.GetInnerTubeVersion())
  • 52 |
  • @localization.GetString("error.body.details.path") (@path)
  • 53 |
  • @localization.GetString("error.body.details.locale") (@Context.GetInnerTubeLanguage()_@Context.GetInnerTubeRegion())
  • 54 |
  • @localization.GetString("error.body.details.stacktrace")
  • 55 |
56 | 57 | @Html.Raw(string.Join("
", 58 | exception.ToString() 59 | .Split('\n') 60 | .Where(x => !x.Contains("Microsoft.AspNetCore.")))) 61 |
62 | } 63 | 64 |

65 | @localization.GetString("error.alternatives") 66 | 67 | Piped 68 | , 69 | 70 | Invidious 71 | , 72 | 73 | YouTube 74 | 75 |

76 | 77 | -------------------------------------------------------------------------------- /LightTube/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model HomepageContext 2 | @using Humanizer 3 |
4 |

@Configuration.RandomMessage()

5 |
6 | @if (Model.Videos is not null) 7 | { 8 |
9 | @foreach (FeedVideo video in Model.Videos) 10 | { 11 |
12 | 13 | @video.Title 14 | 15 |
16 | 17 | @video.Title 18 | 19 | 24 |
25 | @Model.Localization.FormatString("video.subtitle", video.ViewCount.ToString("N0"), video.PublishedDate.Humanize(DateTimeOffset.UtcNow, Model.Localization.GetCulture())) 26 |
27 |
28 |
29 | } 30 |
31 | } 32 | -------------------------------------------------------------------------------- /LightTube/Views/Home/Rss.cshtml: -------------------------------------------------------------------------------- 1 | @model BaseContext 2 | 3 | @{ 4 | Model.Title = Model.Localization.GetRawString("rss.title"); 5 | } 6 | 7 |

@Model.Localization.GetString("rss.explain.title")

8 | 9 |

@Model.Localization.GetString("rss.explain.title")

10 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | 22 | 25 | 34 | 35 | 36 | 39 | 42 | 43 | 44 |
15 | @Model.Localization.GetString("rss.explain.url") 16 | 18 | https://@Context.Request.Host/feed/rss.xml 19 |
23 | @Model.Localization.GetString("rss.explain.username") 24 | 26 | @if (Model.User is not null) { 27 | @Model.User.UserID 28 | } 29 | else 30 | { 31 | @Model.Localization.GetString("rss.explain.username.explain") 32 | } 33 |
37 | @Model.Localization.GetString("rss.explain.password") 38 | 40 | @Model.Localization.GetString("rss.explain.password.explain") 41 |
45 | 46 |

@Model.Localization.GetString("rss.explain.authentication")

47 | 48 | @if (Model.User is not null) { 49 | https://@Model.User.UserID:<@Model.Localization.GetString("rss.explain.password.url")>@@@Context.Request.Host/feed/rss.xml 50 | } 51 | else 52 | { 53 | https://<@Model.Localization.GetString("rss.explain.username.url")>:<@Model.Localization.GetString("rss.explain.password.url")>@@@Context.Request.Host/feed/rss.xml 54 | } -------------------------------------------------------------------------------- /LightTube/Views/OAuth2/Authorize.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web 2 | @using Microsoft.AspNetCore.Http.Extensions 3 | @model LightTube.Contexts.OAuthContext 4 | 5 | @{ 6 | Layout = "_AccountLayout"; 7 | } 8 | 9 | @if (Model.Error is null) 10 | { 11 |

12 | @Model.Localization.FormatString("oauth2.user", Model.User!.UserID) 13 |

14 |

15 | @Model.Localization.FormatString("oauth2.user", Model.Name) 16 |

17 | @Model.Localization.GetString("oauth2.scopes") 18 |

19 | 20 |
21 | @foreach (string scope in Utils.GetScopeDescriptions(Model.Scopes, Model.Localization)) 22 | { 23 | @if (scope.StartsWith("!")) 24 | { 25 |
26 | 27 | 28 | 29 | @scope[1..] 30 |
31 | } 32 | else 33 | { 34 |
35 | 36 | 37 | 38 | @scope 39 |
40 | } 41 | } 42 |
43 | 44 | 45 | 49 | } -------------------------------------------------------------------------------- /LightTube/Views/Settings/Account.cshtml: -------------------------------------------------------------------------------- 1 | @model BaseContext 2 | 3 | @{ 4 | Layout = "_SettingsLayout"; 5 | Model.Title = Model.Localization.GetRawString("settings.account.htmltitle"); 6 | } 7 | 8 |

@Model.Localization.GetString("settings.account.title")

9 | 10 | 11 | 12 | @Model.Localization.GetString("settings.account.delete") 13 | 14 | -------------------------------------------------------------------------------- /LightTube/Views/Settings/ImportExport.cshtml: -------------------------------------------------------------------------------- 1 | @model ImportContext 2 | 3 | @{ 4 | Layout = "_SettingsLayout"; 5 | Model.Title = Model.Localization.GetRawString("settings.import.htmltitle"); 6 | } 7 | 8 | @if (Model.Message != null) 9 | { 10 |
11 | @Html.Raw(Model.Message.Replace("\n", "
")) 12 |
13 | } 14 | 15 |

@Model.Localization.GetString("settings.import.title")

16 | 17 |
18 |
19 |
20 | @Model.Localization.GetString("settings.import.file.title") 21 |
22 |
23 | @Model.Localization.GetString("settings.import.file.subtitle") 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | 33 |

@Model.Localization.GetString("settings.import.export.title")

34 | @Model.Localization.GetString("settings.import.export.json") 35 | 36 |

@Model.Localization.GetString("settings.import.supported.title")

37 |
    38 | @Model.Localization.GetString("settings.import.supported.body") 39 |
40 |

41 | @Model.Localization.GetString("settings.import.supported.more") 42 |

-------------------------------------------------------------------------------- /LightTube/Views/Shared/AccountDropdown.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web 2 | @model BaseContext 3 | @{ 4 | List generalItems = 5 | [ 6 | new DropdownItem(Model.Localization.GetRawString("layout.dropdown.settings"), "/settings/appearance", "gear"), 7 | new DropdownItem(Model.Localization.GetRawString("layout.dropdown.about"), "https://github.com/lighttube-org/lighttube", "info-circle") 8 | ]; 9 | } 10 | 11 | @if (Model.User is not null) 12 | { 13 | 64 | } 65 | else 66 | { 67 | 108 | } -------------------------------------------------------------------------------- /LightTube/Views/Shared/ChannelTabItem.cshtml: -------------------------------------------------------------------------------- 1 | @using InnerTube 2 | @using ChannelTab = InnerTube.Models.ChannelTab 3 | @model (ChannelTab Tab, string Id, bool HasAbout, LightTube.Localization.LocalizationManager Localization) 4 | @{ 5 | string className = "channel-tabs__item"; 6 | 7 | if (Model.Tab.Selected && !Model.HasAbout) 8 | { 9 | className += " active"; 10 | } 11 | } 12 | @Model.Localization.GetString("channel.tab." + Model.Tab.Tab.ToString().ToLower()) -------------------------------------------------------------------------------- /LightTube/Views/Shared/GuideItem.cshtml: -------------------------------------------------------------------------------- 1 | @model GuideItem 2 | @{ 3 | string className = "guide-item"; 4 | string iconId = Model.IconId; 5 | 6 | if (Model.VisibleOnMinifiedGuide) 7 | className += " guide-item__small"; 8 | if (Context.Request.Path.Value! == Model.Path) 9 | { 10 | iconId += "-fill"; 11 | className += " guide-item__active"; 12 | } 13 | } 14 | 15 |
16 | 17 | 18 | 19 |
20 |
21 | @Model.Title 22 |
23 |
-------------------------------------------------------------------------------- /LightTube/Views/Shared/Icons.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LightTube/Views/Shared/Renderers/PostAttachmentRenderer.cshtml: -------------------------------------------------------------------------------- 1 | @using InnerTube.Models 2 | @using InnerTube.Protobuf 3 | @using InnerTube.Renderers 4 | @model (RendererContainer Renderer, LightTube.Localization.LocalizationManager Localization) 5 | 6 | @switch (Model.Renderer.Type) 7 | { 8 | case "communityPostImage": 9 | CommunityPostImageRendererData image = (CommunityPostImageRendererData)Model.Renderer.Data; 10 | if (image.Images.Length == 1) 11 | { 12 |
13 | 14 |
15 | } 16 | else 17 | { 18 |
19 | @foreach (Thumbnail[] img in image.Images) 20 | { 21 | 22 | } 23 |
24 | } 25 | break; 26 | case "video": 27 | VideoRendererData video = (VideoRendererData)Model.Renderer.Data; 28 |
29 | 30 | @video.Title 31 |
32 | @video.Duration.ToDurationString() 33 |
34 |
35 |
36 | 37 | @video.Title 38 | 39 |
40 |
41 | @video.ViewCountText • @video.PublishedText 42 |
43 | @if (video.Author != null) 44 | { 45 |
46 | @video.Author.Title 47 | 48 | @video.Author.Title 49 | 50 | @foreach (Badge channelBadge in video.Author.Badges ?? []) 51 | { 52 | 53 | 54 | 55 | } 56 |
57 | } 58 |
59 | @Html.Raw(video.Description) 60 |
61 |
62 | @foreach (Badge videoBadge in video.Badges) 63 | { 64 | 65 | @videoBadge.Label 66 | 67 | } 68 |
69 |
70 |
71 |
72 | break; 73 | case "exception": 74 | ExceptionRendererData e = (ExceptionRendererData)Model.Renderer.Data; 75 |
76 | @e.Message while converting @e.RendererCase 77 |
78 | break; 79 | case "unknown": 80 |
81 |
Unknown RendererContainer.OriginalType: @Model.Renderer.OriginalType
82 | @if (Model.Renderer.OriginalType == "UnknownProtobufRenderer") 83 | { 84 | @Convert.ToBase64String((Model.Renderer.Data as UnknownRendererData)?.ProtobufBytes ?? []) 85 | } 86 | else 87 | { 88 | @((Model.Renderer.Data as UnknownRendererData)?.Json) 89 | } 90 |
91 | break; 92 | default: 93 |
Unexpected RendererContainer.Type: @Model.Renderer.Type
94 | break; 95 | } -------------------------------------------------------------------------------- /LightTube/Views/Shared/Renderers/ReelRenderer.cshtml: -------------------------------------------------------------------------------- 1 | @model (InnerTube.Renderers.VideoRendererData Video, LightTube.Localization.LocalizationManager Localization) 2 | 3 | 4 |
5 | @Model.Video.Title 6 |
7 |
8 |
@Model.Video.Title
9 |
@Model.Video.ViewCountText
10 |
11 |
-------------------------------------------------------------------------------- /LightTube/Views/Shared/SettingsTab.cshtml: -------------------------------------------------------------------------------- 1 | @model SettingsTab 2 | @{ 3 | string className = "settings-tab"; 4 | 5 | if (Context.Request.Path.Value! == Model.Path) 6 | className += " active"; 7 | } 8 | @Model.Label -------------------------------------------------------------------------------- /LightTube/Views/Shared/SubscribeButton.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Database.Models 2 | @model (string ChannelId, LightTube.Localization.LocalizationManager Localization) 3 | @{ 4 | SubscriptionType type = Context.GetSubscriptionType(Model.ChannelId); 5 | } 6 | 7 | 8 | @switch (type) 9 | { 10 | case SubscriptionType.NOTIFICATIONS_ON: 11 | 12 | @Model.Localization.GetString("subscription.button.unsubscribe") 13 | 14 | 15 | 16 | 17 | 18 | 19 | break; 20 | case SubscriptionType.NOTIFICATIONS_OFF: 21 | 22 | @Model.Localization.GetString("subscription.button.unsubscribe") 23 | 24 | 25 | 26 | 27 | 28 | 29 | break; 30 | case SubscriptionType.NONE: 31 | default: 32 | 33 | @Model.Localization.GetString("subscription.button.subscribe") 34 | 35 | break; 36 | } -------------------------------------------------------------------------------- /LightTube/Views/Shared/_AccountLayout.cshtml: -------------------------------------------------------------------------------- 1 | @model AccountContext 2 | 3 | 4 | 5 | 6 | @Model.HtmlTitle - LightTube 7 | 8 | 9 | 10 | 11 | 12 | 13 | 30 | 31 | 37 | 38 | -------------------------------------------------------------------------------- /LightTube/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Html 2 | @using System.Web 3 | @using Microsoft.AspNetCore.Http.Extensions 4 | @model BaseContext 5 | 6 | 7 | 8 | 9 | 10 | @(string.IsNullOrWhiteSpace(Model.Title) ? "LightTube" : $"{Model.Title} - LightTube") 11 | 12 | 13 | @foreach (IHtmlContent item in Model.HeadTags) 14 | { 15 | @item 16 | } 17 | 18 | 19 | 20 | 21 | @foreach (IHtmlContent item in Model.rssElement) 22 | { 23 | @item 24 | } 25 | @if (Configuration.CustomCssPath != null) 26 | { 27 | 28 | } 29 | 30 | 31 | 57 |
58 | 59 | 60 | 61 |
62 | 67 |
68 |
69 | @if (Utils.ShouldShowAlert(Context.Request)) 70 | { 71 |
72 |
73 | @Configuration.Alert 74 |
75 | 76 | 77 | 78 | 79 | 80 |
81 | } 82 | @RenderBody() 83 |
84 | @foreach (IHtmlContent item in Model.EndTags) 85 | { 86 | @item 87 | } 88 | 89 | -------------------------------------------------------------------------------- /LightTube/Views/Shared/_ModalLayout.cshtml: -------------------------------------------------------------------------------- 1 | @model ModalContext 2 | 3 | 4 | 5 | 6 | 7 | @Model.Title - LightTube 8 | 9 | 10 | 11 | 12 | 13 | 14 | @if (Configuration.CustomCssPath != null) 15 | { 16 | 17 | } 18 | 19 | 20 | 52 | 53 | -------------------------------------------------------------------------------- /LightTube/Views/Shared/_SettingsLayout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Html 2 | @model BaseContext 3 | 4 | @{ 5 | SettingsTab[] tabs = 6 | [ 7 | new SettingsTab(Model.Localization.GetRawString("settings.account.htmltitle"), "/settings/account"), 8 | new SettingsTab(Model.Localization.GetRawString("settings.appearance.title"), "/settings/appearance"), 9 | new SettingsTab(Model.Localization.GetRawString("settings.import.htmltitle"), "/settings/data") 10 | ]; 11 | } 12 | 13 | 14 | 15 | 16 | 17 | @Model.Title - LightTube 18 | 19 | 20 | @foreach (IHtmlContent item in Model.HeadTags) 21 | { 22 | @item 23 | } 24 | 25 | 26 | 27 | 28 | @if (Configuration.CustomCssPath != null) 29 | { 30 | 31 | } 32 | 33 | 34 |
35 | 41 |
42 | 50 |
51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |

Settings

59 | @foreach (SettingsTab tab in tabs) 60 | { 61 | 62 | } 63 |
64 |
65 | @RenderBody() 66 |
67 |
68 |
69 | @foreach (IHtmlContent item in Model.EndTags) 70 | { 71 | @item 72 | } 73 | 74 | -------------------------------------------------------------------------------- /LightTube/Views/Youtube/Download.cshtml: -------------------------------------------------------------------------------- 1 | @using InnerTube.Models 2 | @using InnerTube.Protobuf.Responses 3 | @model PlaylistVideoContext 4 | @{ 5 | Layout = "_ModalLayout"; 6 | } 7 | 8 | 21 | 22 |

23 | @Model.Localization.GetString("download.format.select") 24 |

25 | 26 | 27 | 28 | 33 | 36 | 37 | 38 | 43 | 46 | 47 |
29 | 30 | 31 | 32 | 34 | @Model.Localization.GetString("download.direct") 35 |
39 | 40 | 41 | 42 | 44 | @Model.Localization.GetString("download.proxy") 45 |
48 | 49 |
50 | 51 | @foreach (Format f in Model.Extra!.Formats) 52 | { 53 |
54 |
55 |
56 | @f.QualityLabel 57 |
58 |
59 | @f.Fps FPS, @(f.AudioSampleRate)Hz, @(f.Width)x@(f.Height) (@(f.Mime.Split(";")[0])) 60 |
61 |
62 |
63 | 76 |
77 |
78 | } 79 | 80 | @foreach (Format f in Model.Extra!.AdaptiveFormats) 81 | { 82 |
83 |
84 | @if (f.Mime.StartsWith("video")) { 85 |
86 | @Model.Localization.GetString("download.format.video") - @f.QualityLabel 87 |
88 |
89 | @f.Fps FPS, @(f.Width)x@(f.Height) (@(f.Mime.Split(";")[0])) 90 |
91 | } else { 92 |
93 | @Model.Localization.GetString("download.format.audio") - Audio Quality: @f.AudioQuality 94 |
95 |
96 | @f.AudioChannels channels, @(f.AudioSampleRate)Hz (@(f.Mime.Split(";")[0])) 97 |
98 | } 99 |
100 |
101 | 114 |
115 |
116 | } -------------------------------------------------------------------------------- /LightTube/Views/Youtube/Embed.cshtml: -------------------------------------------------------------------------------- 1 | @model EmbedContext 2 | 3 | @{ 4 | Layout = null; 5 | Model.Title = Model.Player.Title; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @Model.Player.Player?.Details.Title - LightTube 28 | 39 | 40 | 41 | @await Html.PartialAsync("Player", Model.Player) 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /LightTube/Views/Youtube/Playlist.cshtml: -------------------------------------------------------------------------------- 1 | @using InnerTube.Renderers 2 | @using LightTube.CustomRendererDatas 3 | @using Microsoft.AspNetCore.Http.Extensions 4 | @using Microsoft.Extensions.Primitives 5 | @model PlaylistContext 6 | 7 | @{ 8 | Model.Title = Model.PlaylistTitle; 9 | } 10 | 11 |
12 |
13 |
14 | @Model.PlaylistTitle 15 |
16 |

@Model.PlaylistTitle

17 | @Model.AuthorName 18 |
19 | @Model.ViewCountText  @Model.LastUpdatedText 20 |

21 | @Html.Raw(Model.PlaylistDescription) 22 |
23 | @if (Model.Editable) 24 | { 25 | 37 | } 38 |
39 |
40 | @foreach (string alert in Model.Alerts) 41 | { 42 |
43 |
44 | @alert 45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 | } 53 | @foreach (RendererContainer renderer in Model.Items) 54 | { 55 | switch (renderer.Type) 56 | { 57 | case "video": 58 | PlaylistVideoRendererData video = (PlaylistVideoRendererData)renderer.Data; 59 |
60 |
61 | @video.VideoIndexText 62 |
63 | @{ 64 | Context.Request.Query.TryGetValue("list", out StringValues l); 65 | } 66 | 67 | @video.Title 68 |
69 | @video.Duration.ToDurationString() 70 |
71 |
72 |
73 | @video.Title 74 | @if (video.Author != null) 75 | { 76 | 79 | } 80 |
81 |
82 |
83 | @if (video is EditablePlaylistVideoRendererData { Editable: true }) 84 | { 85 | 86 | 87 | 88 | 89 | 90 | } 91 |
92 |
93 | break; 94 | case "exception": 95 | ExceptionRendererData e = (ExceptionRendererData)renderer.Data; 96 |
97 | @e.Message while converting @e.RendererCase 98 |
99 | break; 100 | } 101 | } 102 |
103 |
104 |
105 | 106 | 107 | @if (Model.Continuation is not null) 108 | { 109 | @Model.Localization.GetString("pagination.next") 110 | } 111 | else 112 | { 113 | 114 | } 115 |
-------------------------------------------------------------------------------- /LightTube/Views/Youtube/Subscription.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube.Database.Models 2 | @model SubscriptionContext 3 | 4 | @{ 5 | Layout = "_ModalLayout"; 6 | } 7 | 8 | @Model.Channel.Header?.Title 12 |

13 | @Model.Channel.Header?.Title 14 |

15 |

16 | @Model.Localization.GetString("subscription.edit.body") 17 |

18 | 19 |
20 | 21 |
24 | 25 |
28 | 29 | 32 |
33 | -------------------------------------------------------------------------------- /LightTube/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using LightTube 2 | @using LightTube.Models 3 | @using LightTube.Contexts 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -------------------------------------------------------------------------------- /LightTube/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } -------------------------------------------------------------------------------- /LightTube/YoutubeRss.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace LightTube; 4 | 5 | public static class YoutubeRss 6 | { 7 | private static HttpClient httpClient = new(); 8 | 9 | public static async Task GetChannelFeed(string channelId) 10 | { 11 | HttpResponseMessage response = 12 | await httpClient.GetAsync("https://www.youtube.com/feeds/videos.xml?channel_id=" + channelId); 13 | if (!response.IsSuccessStatusCode) 14 | return new ChannelFeed( 15 | $"Failed to get channel videos: HTTP {(int)response.StatusCode}", 16 | channelId, 17 | [] 18 | ); 19 | 20 | string xml = await response.Content.ReadAsStringAsync(); 21 | XDocument doc = XDocument.Parse(xml); 22 | 23 | ChannelFeed feed = new( 24 | doc.Descendants().First(p => p.Name.LocalName == "title").Value, 25 | doc.Descendants().First(p => p.Name.LocalName == "channelId").Value, doc 26 | .Descendants() 27 | .Where(p => p.Name.LocalName == "entry") 28 | .Select(x => new FeedVideo( 29 | x.Descendants().First(p => p.Name.LocalName == "videoId").Value, 30 | x.Descendants().First(p => p.Name.LocalName == "title").Value, 31 | x.Descendants().First(p => p.Name.LocalName == "description").Value, 32 | long.Parse(x.Descendants().First(p => p.Name.LocalName == "statistics").Attribute("views")?.Value ?? 33 | "-1"), 34 | x.Descendants().First(p => p.Name.LocalName == "thumbnail").Attribute("url")?.Value ?? 35 | $"https://i.ytimg.com/vi/{x.Descendants().First(p => p.Name.LocalName == "videoId").Value}/hqdefault.jpg", 36 | x.Descendants().First(p => p.Name.LocalName == "name").Value, 37 | x.Descendants().First(p => p.Name.LocalName == "channelId").Value, 38 | DateTimeOffset.Parse(x.Descendants().First(p => p.Name.LocalName == "published").Value) 39 | )) 40 | ); 41 | 42 | return feed; 43 | } 44 | 45 | public static async Task GetMultipleFeeds(IEnumerable channelIds) 46 | { 47 | Task[] feeds = channelIds.Select(GetChannelFeed).ToArray(); 48 | await Task.WhenAll(feeds); 49 | 50 | List videos = []; 51 | foreach (ChannelFeed feed in feeds.Select(x => x.Result)) videos.AddRange(feed.Videos); 52 | 53 | videos.Sort((a, b) => DateTimeOffset.Compare(b.PublishedDate, a.PublishedDate)); 54 | return videos.ToArray(); 55 | } 56 | } 57 | 58 | public class ChannelFeed(string id, string name, IEnumerable videos) 59 | { 60 | public string Name = name; 61 | public string Id = id; 62 | public FeedVideo[] Videos = videos.ToArray(); 63 | } 64 | 65 | public class FeedVideo( 66 | string id, 67 | string title, 68 | string description, 69 | long viewCount, 70 | string thumbnail, 71 | string channelName, 72 | string channelId, 73 | DateTimeOffset publishedDate) 74 | { 75 | public string Id = id; 76 | public string Title = title; 77 | public string Description = description; 78 | public long ViewCount = viewCount; 79 | public string Thumbnail = thumbnail; 80 | public string ChannelName = channelName; 81 | public string ChannelId = channelId; 82 | public DateTimeOffset PublishedDate = publishedDate; 83 | } -------------------------------------------------------------------------------- /LightTube/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LightTube/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": { 4 | "Default": "Information", 5 | "Override": { 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | }, 11 | "AllowedHosts": "*" 12 | } -------------------------------------------------------------------------------- /LightTube/wwwroot/css/account.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 700px) { 2 | html, body { 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .signin-box { 9 | border: 1px solid #dadce0; 10 | border-radius: 8px; 11 | } 12 | } 13 | 14 | .signin-container { 15 | height: 100%; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | * { 22 | font-family: "Roboto", sans-serif; 23 | } 24 | 25 | .signin-box { 26 | width: 400px; 27 | padding: 25px; 28 | } 29 | 30 | .signin-title { 31 | text-align: center; 32 | font-weight: 700; 33 | font-size: 1.5rem; 34 | } 35 | 36 | .signin-error { 37 | padding: 4px 16px; 38 | color: red; 39 | font-weight: bold; 40 | } 41 | 42 | .signin-input-container { 43 | margin: 8px; 44 | } 45 | 46 | input.text { 47 | padding: 13px 15px; 48 | width: calc(100% - 25px); 49 | border: 1px solid #dadce0; 50 | border-radius: 4px; 51 | transition: all 100ms; 52 | } 53 | 54 | input.text:active { 55 | padding: 12px 14px; 56 | border: 2px solid #1a73c8; 57 | } 58 | 59 | input[type=checkbox] { 60 | width: 24px; 61 | height: 24px; 62 | flex-shrink: 0; 63 | } 64 | 65 | .signin-checkbox { 66 | display: flex; 67 | align-items: center; 68 | gap: 8px; 69 | } 70 | 71 | .signin-buttons { 72 | margin-top: 32px; 73 | display: flex; 74 | justify-content: space-between; 75 | } 76 | 77 | [class^="signin-button-"] { 78 | font-weight: bold; 79 | line-height: 36px; 80 | font-size: 13px; 81 | transition: background-color 100ms; 82 | border-radius: 4px; 83 | } 84 | 85 | .signin-button-primary { 86 | padding: 0 24px; 87 | background-color: #1a73e8; 88 | color: #fff; 89 | border: none; 90 | } 91 | 92 | .signin-button-primary:hover { 93 | background-color: #1b66c8; 94 | } 95 | 96 | .signin-button-primary:active { 97 | background-color: #1c539c; 98 | } 99 | 100 | .signin-button-danger { 101 | padding: 0 24px; 102 | background-color: hsl(0, 82%, 51%); 103 | color: #fff; 104 | border: none; 105 | } 106 | 107 | .signin-button-danger:hover { 108 | background-color: hsl(0, 76%, 45%); 109 | } 110 | 111 | .signin-button-danger:active { 112 | background-color: hsl(0, 70%, 36%); 113 | } 114 | 115 | .signin-button-secondary { 116 | padding: 0 8px; 117 | text-decoration: none; 118 | color: #1a73e8; 119 | } 120 | 121 | .signin-button-secondary:hover { 122 | background-color: #f6fafe; 123 | color: #174ea6; 124 | } 125 | 126 | .signin-button-secondary:active { 127 | background-color: #cadff9; 128 | } 129 | 130 | .scope-list { 131 | margin-top: 24px; 132 | display: flex; 133 | flex-direction: column; 134 | gap: 12px; 135 | } 136 | 137 | .scope { 138 | display: flex; 139 | gap: 12px; 140 | line-height: 24px; 141 | } 142 | 143 | .scope-icon { 144 | width: 24px; 145 | height: 24px; 146 | } -------------------------------------------------------------------------------- /LightTube/wwwroot/css/modal.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | @media screen and (min-width: 700px) { 8 | .modal-box { 9 | border: 1px solid var(--border-color); 10 | border-radius: 8px; 11 | width: 400px; 12 | max-height: 75%; 13 | } 14 | } 15 | 16 | @media screen and (max-width: 700px) { 17 | 18 | .modal-box { 19 | border-radius: 8px; 20 | width: 100%; 21 | height: 100%; 22 | } 23 | } 24 | 25 | .modal-box { 26 | display: flex; 27 | flex-direction: column; 28 | min-height: 400px; 29 | gap: 8px; 30 | } 31 | 32 | .modal-container { 33 | height: 100%; 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | } 38 | 39 | .modal-content { 40 | display: flex; 41 | flex-direction: column; 42 | align-items: center; 43 | overflow: auto; 44 | flex-grow: 1; 45 | padding: 0 16px; 46 | max-width: calc(100% - 32px); 47 | } 48 | 49 | .avatar { 50 | border-radius: 50%; 51 | overflow: hidden; 52 | width: 128px; 53 | height: 128px; 54 | } 55 | 56 | label { 57 | color: var(--text-primary); 58 | } 59 | 60 | .modal-content.center { 61 | justify-content: center; 62 | align-items: center; 63 | width: 100%; 64 | height: 100%; 65 | } 66 | 67 | .modal-content > p { 68 | text-align: center; 69 | } 70 | 71 | .modal-title { 72 | border-bottom: 1px solid var(--border-color); 73 | font-size: 1.5rem; 74 | color: var(--text-primary); 75 | padding: 8px; 76 | margin: 0; 77 | font-weight: normal; 78 | } 79 | 80 | .modal-buttons { 81 | border-top: 1px solid var(--border-color); 82 | display: flex; 83 | gap: 8px; 84 | padding: 8px; 85 | } 86 | 87 | [class^="modal-button-"] { 88 | font-weight: bold; 89 | line-height: 36px; 90 | font-size: 13px; 91 | transition: background-color 100ms; 92 | border-radius: 4px; 93 | } 94 | 95 | .modal-button-primary { 96 | padding: 0 24px; 97 | background-color: #1a73e8; 98 | color: #fff; 99 | border: none; 100 | } 101 | 102 | .modal-button-primary:hover { 103 | background-color: #1b66c8; 104 | } 105 | 106 | .modal-button-primary:active { 107 | background-color: #1c539c; 108 | } 109 | 110 | .modal-button-danger { 111 | padding: 0 24px; 112 | background-color: hsl(0, 82%, 51%); 113 | color: #fff; 114 | border: none; 115 | } 116 | 117 | .modal-button-danger:hover { 118 | background-color: hsl(0, 76%, 45%); 119 | } 120 | 121 | .modal-button-danger:active { 122 | background-color: hsl(0, 70%, 36%); 123 | } 124 | 125 | .modal-button-secondary { 126 | padding: 0 8px; 127 | text-decoration: none; 128 | color: #1a73e8; 129 | } 130 | 131 | .modal-button-secondary:hover { 132 | background-color: #f6fafe22; 133 | color: #174ea6; 134 | } 135 | 136 | .modal-button-secondary:active { 137 | background-color: #cadff9; 138 | } 139 | 140 | .modal-item { 141 | display: flex; 142 | flex-direction: row; 143 | padding: 8px; 144 | gap: 8px; 145 | background-color: var(--item-active-background); 146 | width: 350px; 147 | border-radius: 8px; 148 | } 149 | 150 | .modal-item__thumbnail { 151 | height: 70px; 152 | width: 125px; 153 | aspect-ratio: 16 / 9; 154 | overflow: clip; 155 | border-radius: 8px; 156 | flex-shrink: 0; 157 | flex-grow: 0; 158 | } 159 | 160 | .modal-item__thumbnail img { 161 | width: 100%; 162 | height: 100%; 163 | object-fit: cover; 164 | } -------------------------------------------------------------------------------- /LightTube/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/favicon.ico -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Black.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Black.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-BlackItalic.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-BlackItalic.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Bold.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Bold.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-BoldItalic.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-BoldItalic.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Italic.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Italic.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Light.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Light.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-LightItalic.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-LightItalic.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Medium.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Medium.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-MediumItalic.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-MediumItalic.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Regular.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Regular.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Thin.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-Thin.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-ThinItalic.woff -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/Roboto-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/fonts/roboto/Roboto-ThinItalic.woff2 -------------------------------------------------------------------------------- /LightTube/wwwroot/fonts/roboto/roboto.css: -------------------------------------------------------------------------------- 1 | /* See /fonts/roboto/LICENSE.txt for license information */ 2 | @font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-Italic.woff2') format('woff2'), url('/fonts/roboto/Roboto-Italic.woff') format('woff');font-weight:normal;font-style:italic;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-Medium.woff2') format('woff2'), url('/fonts/roboto/Roboto-Medium.woff') format('woff');font-weight:500;font-style:normal;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-Light.woff2') format('woff2'), url('/fonts/roboto/Roboto-Light.woff') format('woff');font-weight:300;font-style:normal;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-LightItalic.woff2') format('woff2'), url('/fonts/roboto/Roboto-LightItalic.woff') format('woff');font-weight:300;font-style:italic;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-ThinItalic.woff2') format('woff2'), url('/fonts/roboto/Roboto-ThinItalic.woff') format('woff');font-weight:100;font-style:italic;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-MediumItalic.woff2') format('woff2'), url('/fonts/roboto/Roboto-MediumItalic.woff') format('woff');font-weight:500;font-style:italic;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-Regular.woff2') format('woff2'), url('/fonts/roboto/Roboto-Regular.woff') format('woff');font-weight:normal;font-style:normal;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-Thin.woff2') format('woff2'), url('/fonts/roboto/Roboto-Thin.woff') format('woff');font-weight:100;font-style:normal;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-Bold.woff2') format('woff2'), url('/fonts/roboto/Roboto-Bold.woff') format('woff');font-weight:bold;font-style:normal;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-BlackItalic.woff2') format('woff2'), url('/fonts/roboto/Roboto-BlackItalic.woff') format('woff');font-weight:900;font-style:italic;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-Black.woff2') format('woff2'), url('/fonts/roboto/Roboto-Black.woff') format('woff');font-weight:900;font-style:normal;font-display:optional}@font-face{font-family:'Roboto';src:url('/fonts/roboto/Roboto-BoldItalic.woff2') format('woff2'), url('/fonts/roboto/Roboto-BoldItalic.woff') format('woff');font-weight:bold;font-style:italic;font-display:optional} -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-114x114.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-120x120.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-144x144.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-152x152.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-16x16.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-180x180.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-192x192.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-32x32.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-36x36.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-48x48.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-57x57.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-60x60.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-72x72.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-76x76.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-96x96.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/favicon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/favicon-precomposed.png -------------------------------------------------------------------------------- /LightTube/wwwroot/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/LightTube/wwwroot/icons/icon.png -------------------------------------------------------------------------------- /LightTube/wwwroot/js/player.js: -------------------------------------------------------------------------------- 1 | try { 2 | document.querySelector(".player-container").style.marginBottom = "0"; 3 | } catch (_) { 4 | 5 | } 6 | 7 | const oldVol = localStorage.getItem("ltvideo.volume"); 8 | if (oldVol) { 9 | localStorage.setItem("ltplayer.volume", oldVol); 10 | localStorage.removeItem("ltvideo.volume"); 11 | } 12 | 13 | const player = new Player(`video#${playerId}`, videoInfo, playtype); 14 | 15 | document.querySelectorAll("a[href*='?v="+videoId+"&t=']").forEach(x => { 16 | const t = Number(x.getAttribute("href").split("t=")[1].replace(/[^0-9]/, "")); 17 | x.onclick = e => { 18 | e.preventDefault(); 19 | player.player.currentTime = t; 20 | player.player.scrollIntoView({behavior: "smooth"}); 21 | }; 22 | }); -------------------------------------------------------------------------------- /LightTube/wwwroot/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /LightTube/wwwroot/svg/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /OTHERLIBS.md: -------------------------------------------------------------------------------- 1 | # Libraries that are used in LightTube 2 | 3 | | Project Name | Description & Where/Why it is used | Source Code URL | 4 | | --------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | 5 | | HLS.js | HLS.js is a JavaScript library that plays HLS in browsers with support for MSE.
Used to integrate with LTPlayer | https://github.com/video-dev/hls.js/ | 6 | | Bootstrap Icons | Official open source SVG icon library for Bootstrap.
Used for the icons throughout the website | https://github.com/twbs/icons/ | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

LightTube

3 |

A lightweight, privacy respecting alternative frontend for YouTube

4 |
5 | 6 | > [!CAUTION] 7 | > This project is no longer under development. While the latest version MIGHT work for some time under residential IPs, 8 | > there will be no future updates and support for the foreseeable future. Use at your own risk. 9 | > 10 | > https://blog.kuylar.dev/lighttube-end-of-development/ 11 | 12 | ## Features 13 | 14 | - Lightweight 15 | - Ad-free 16 | - Dislike counts from [Return YouTube Dislike](https://www.returnyoutubedislike.com/) 17 | - [SponsorBlock](https://sponsor.ajay.app/) support 18 | - Subscriptions feed without the requirement of a Google account 19 | - Playlists 20 | - Proxying the videos through LightTube 21 | 22 | ## Translations [![Translation status](https://hosted.weblate.org/widget/lighttube/web/svg-badge.svg)](https://hosted.weblate.org/engage/lighttube/) 23 | 24 | [![Translation status](https://hosted.weblate.org/widget/lighttube/web/horizontal-auto.svg)](https://hosted.weblate.org/engage/lighttube/) 25 | 26 | Interested at translating LightTube to your language? Head over 27 | to [Weblate](https://hosted.weblate.org/engage/lighttube/)! 28 | 29 | ## Screenshots 30 | 31 | ### Desktop 32 | 33 | | Page | Light | Dark | 34 | |:-------|:--------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------:| 35 | | Search | ![image](https://github.com/lighttube-org/LightTube/blob/master/screenshots/desktop/light/search.png?raw=true) | ![image](https://github.com/lighttube-org/LightTube/blob/master/screenshots/desktop/dark/search.png?raw=true) | 36 | | Video | ![image](https://github.com/lighttube-org/LightTube/blob/master/screenshots/desktop/light/video.png?raw=true) | ![image](https://github.com/lighttube-org/LightTube/blob/master/screenshots/desktop/dark/video.png?raw=true) | 37 | 38 | ### Mobile 39 | 40 | | Page | Light | Dark | 41 | |:-------|:-------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------:| 42 | | Search | ![image](https://github.com/lighttube-org/LightTube/blob/master/screenshots/mobile/light/search.png?raw=true) | ![image](https://github.com/lighttube-org/LightTube/blob/master/screenshots/mobile/dark/search.png?raw=true) | 43 | | Video | ![image](https://github.com/lighttube-org/LightTube/blob/master/screenshots/mobile/light/video.png?raw=true) | ![image](https://github.com/lighttube-org/LightTube/blob/master/screenshots/mobile/dark/video.png?raw=true) | 44 | -------------------------------------------------------------------------------- /lighttube-helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /lighttube-helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: lighttube 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /lighttube-helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "lighttube-helm.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "lighttube-helm.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "lighttube-helm.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "lighttube-helm.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /lighttube-helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "lighttube-helm.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "lighttube-helm.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "lighttube-helm.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "lighttube-helm.labels" -}} 37 | helm.sh/chart: {{ include "lighttube-helm.chart" . }} 38 | {{ include "lighttube-helm.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "lighttube-helm.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "lighttube-helm.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "lighttube-helm.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "lighttube-helm.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /lighttube-helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "lighttube-helm.fullname" . }} 5 | labels: 6 | {{- include "lighttube-helm.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "lighttube-helm.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "lighttube-helm.labels" . | nindent 8 }} 22 | {{- with .Values.podLabels }} 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | spec: 26 | {{- with .Values.imagePullSecrets }} 27 | imagePullSecrets: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | serviceAccountName: {{ include "lighttube-helm.serviceAccountName" . }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }} 35 | securityContext: 36 | {{- toYaml .Values.securityContext | nindent 12 }} 37 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 38 | imagePullPolicy: {{ .Values.image.pullPolicy }} 39 | env: 40 | {{ toYaml .Values.env | nindent 12}} 41 | ports: 42 | - name: http 43 | containerPort: {{ .Values.service.port }} 44 | protocol: TCP 45 | livenessProbe: 46 | httpGet: 47 | path: / 48 | port: http 49 | readinessProbe: 50 | httpGet: 51 | path: / 52 | port: http 53 | resources: 54 | {{- toYaml .Values.resources | nindent 12 }} 55 | {{- with .Values.volumeMounts }} 56 | volumeMounts: 57 | {{- toYaml . | nindent 12 }} 58 | {{- end }} 59 | {{- with .Values.volumes }} 60 | volumes: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.nodeSelector }} 64 | nodeSelector: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | {{- with .Values.affinity }} 68 | affinity: 69 | {{- toYaml . | nindent 8 }} 70 | {{- end }} 71 | {{- with .Values.tolerations }} 72 | tolerations: 73 | {{- toYaml . | nindent 8 }} 74 | {{- end }} 75 | -------------------------------------------------------------------------------- /lighttube-helm/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "lighttube-helm.fullname" . }} 6 | labels: 7 | {{- include "lighttube-helm.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "lighttube-helm.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /lighttube-helm/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "lighttube-helm.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "lighttube-helm.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /lighttube-helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "lighttube-helm.fullname" . }} 5 | labels: 6 | {{- include "lighttube-helm.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "lighttube-helm.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /lighttube-helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "lighttube-helm.serviceAccountName" . }} 6 | labels: 7 | {{- include "lighttube-helm.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /lighttube-helm/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "lighttube-helm.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "lighttube-helm.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "lighttube-helm.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /lighttube-helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for lighttube-helm. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: kuylar/lighttube 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "latest" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Automatically mount a ServiceAccount's API credentials? 21 | automount: true 22 | # Annotations to add to the service account 23 | annotations: {} 24 | # The name of the service account to use. 25 | # If not set and create is true, a name is generated using the fullname template 26 | name: "" 27 | 28 | env: 29 | # (Required) MongoDB Database Connection String 30 | - name: LIGHTTUBE_MONGODB_CONNSTR 31 | value: "mongodb://lighttube:lighttube@database:27017/lighttube" 32 | # (Optional) MongoDB Database 33 | - name: LIGHTTUBE_MONGODB_DATABASE 34 | value: "lighttube" 35 | # (Optional) This is the text that will be shown on the home page 36 | # Default: Search something to get started! 37 | - name: LIGHTTUBE_MOTD 38 | value: "Search something to get started!" 39 | # (Optional) Amount of video player data to hold in the cache 40 | # Default: 50 41 | - name: LIGHTTUBE_CACHE_SIZE 42 | value: "50" 43 | # (Optional) Default theme for the interface. Either "light" or "dark" 44 | # Default: light 45 | - name: LIGHTTUBE_DEFAULT_THEME 46 | value: "light" 47 | # (Optional) Disable new users from signing up 48 | # Default: false 49 | - name: LIGHTTUBE_DISABLE_REGISTRATION 50 | value: "false" 51 | # (Optional) Disable the video proxy 52 | # Enable this if you don't want videos to be proxied over LightTube 53 | # This will also disable HLS/DASH playback on browsers 54 | # Default: false 55 | - name: LIGHTTUBE_DISABLE_PROXY 56 | value: "false" 57 | # (Optional) Enable video proxy for 3rd party apps 58 | # Apps may or may not choose to follow this setting 59 | # Default: false 60 | - name: LIGHTTUBE_ENABLE_THIRD_PARTY_PROXY 61 | value: "true" 62 | # (Optional) Default content language (only effects video title/descriptions, 63 | # "### views"/"### subscribers"/"Published # days ago" text on search results etc.) 64 | # WARNING: An invalid value may cause LightTube to not work. Make sure you put in a 65 | # valid language ID 66 | # Default: en 67 | - name: LIGHTTUBE_DEFAULT_CONTENT_LANGUAGE 68 | value: "en" 69 | # (Optional) Default content region (only effects the browse screen. search results 70 | # and recommendations still use the region that the server connects YouTube from) 71 | # WARNING: An invalid value may cause LightTube to not work. Make sure you put in a 72 | # valid region ID 73 | # Default: US 74 | - name: LIGHTTUBE_DEFAULT_CONTENT_REGION 75 | value: "US" 76 | # - name: LIGHTTUBE_CUSTOM_CSS_PATH 77 | # value: 78 | # (Optional) Authentication for age-gated videos 79 | # Follow the following wiki page to get the required values 80 | # https://github.com/lighttube-org/InnerTube/wiki/Authorization 81 | # - name: LIGHTTUBE_AUTH_TYPE: 82 | # value: oauth2 83 | # - name: LIGHTTUBE_AUTH_REFRESH_TOKEN: 84 | # value: 85 | 86 | podAnnotations: {} 87 | podLabels: {} 88 | 89 | podSecurityContext: {} 90 | # fsGroup: 2000 91 | 92 | securityContext: {} 93 | # capabilities: 94 | # drop: 95 | # ALL 96 | # readOnlyRootFilesystem: true 97 | # runAsNonRoot: true 98 | # runAsUser: 1000 99 | 100 | service: 101 | type: ClusterIP 102 | port: 80 103 | 104 | ingress: 105 | enabled: false 106 | className: "" 107 | annotations: {} 108 | # kubernetes.io/ingress.class: nginx 109 | # kubernetes.io/tls-acme: "true" 110 | hosts: 111 | - host: chart-example.local 112 | paths: 113 | - path: / 114 | pathType: ImplementationSpecific 115 | tls: [] 116 | # secretName: chart-example-tls 117 | # hosts: 118 | # chart-example.local 119 | 120 | resources: {} 121 | # We usually recommend not to specify default resources and to leave this as a conscious 122 | # choice for the user. This also increases chances charts run on environments with little 123 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 124 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 125 | # limits: 126 | # cpu: 100m 127 | # memory: 128Mi 128 | # requests: 129 | # cpu: 100m 130 | # memory: 128Mi 131 | 132 | autoscaling: 133 | enabled: false 134 | minReplicas: 1 135 | maxReplicas: 100 136 | targetCPUUtilizationPercentage: 80 137 | # targetMemoryUtilizationPercentage: 80 138 | 139 | # Additional volumes on the output Deployment definition. 140 | volumes: [] 141 | # name: foo 142 | # secret: 143 | # secretName: mysecret 144 | # optional: false 145 | 146 | # Additional volumeMounts on the output Deployment definition. 147 | volumeMounts: [] 148 | # name: foo 149 | # mountPath: "/etc/foo" 150 | # readOnly: true 151 | 152 | nodeSelector: {} 153 | 154 | tolerations: [] 155 | 156 | affinity: {} 157 | -------------------------------------------------------------------------------- /public_instances.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "host": "https://tube.kuylar.dev", 4 | "api": true, 5 | "accounts": true 6 | }, 7 | { 8 | "host": "https://1.youre-either-using-the-old-instance-list.kuylar.dev", 9 | "api": false, 10 | "accounts": false 11 | }, 12 | { 13 | "host": "https://2.or-an-outdated-client.kuylar.dev", 14 | "api": false, 15 | "accounts": false 16 | }, 17 | { 18 | "host": "https://3.please-switch-to-the-new-list-at.kuylar.dev", 19 | "api": false, 20 | "accounts": false 21 | }, 22 | { 23 | "host": "https://lighttube.org/instances", 24 | "api": false, 25 | "accounts": false 26 | }, 27 | { 28 | "host": "https://5.or-update-your-client.kuylar.dev", 29 | "api": false, 30 | "accounts": false 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /screenshots/desktop/dark/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/screenshots/desktop/dark/search.png -------------------------------------------------------------------------------- /screenshots/desktop/dark/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/screenshots/desktop/dark/video.png -------------------------------------------------------------------------------- /screenshots/desktop/light/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/screenshots/desktop/light/search.png -------------------------------------------------------------------------------- /screenshots/desktop/light/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/screenshots/desktop/light/video.png -------------------------------------------------------------------------------- /screenshots/mobile/dark/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/screenshots/mobile/dark/search.png -------------------------------------------------------------------------------- /screenshots/mobile/dark/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/screenshots/mobile/dark/video.png -------------------------------------------------------------------------------- /screenshots/mobile/light/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/screenshots/mobile/light/search.png -------------------------------------------------------------------------------- /screenshots/mobile/light/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lighttube-org/LightTube/3df7a311ab9d76b058ba9f5132350ee505107c3b/screenshots/mobile/light/video.png --------------------------------------------------------------------------------