├── .browserslistrc
├── .dockerignore
├── .env.development
├── .env.test
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .rspec
├── .tool-versions
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── Rakefile
├── app
├── assets
│ ├── config
│ │ └── manifest.js
│ └── images
│ │ ├── .keep
│ │ ├── favicon
│ │ ├── 16.png
│ │ └── 32.png
│ │ └── logo
│ │ ├── navbar.png
│ │ ├── square.png
│ │ └── wide.png
├── channels
│ ├── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
│ ├── broadcasts
│ │ ├── application_broadcast.rb
│ │ └── download_status.rb
│ └── download_status_channel.rb
├── controllers
│ ├── application_controller.rb
│ ├── application_devise_controller.rb
│ ├── concerns
│ │ └── .keep
│ ├── feeds_controller.rb
│ ├── graphql_controller.rb
│ └── vue_apps_controller.rb
├── graphql
│ └── user_api
│ │ ├── mutations
│ │ ├── .keep
│ │ ├── base_mutation.rb
│ │ ├── create_feed.rb
│ │ ├── destroy_feed.rb
│ │ ├── destroy_request.rb
│ │ ├── order_feeds.rb
│ │ ├── request_for_url.rb
│ │ └── update_request.rb
│ │ ├── podify_schema.rb
│ │ └── types
│ │ ├── .keep
│ │ ├── base_argument.rb
│ │ ├── base_enum.rb
│ │ ├── base_field.rb
│ │ ├── base_input_object.rb
│ │ ├── base_interface.rb
│ │ ├── base_object.rb
│ │ ├── base_scalar.rb
│ │ ├── base_union.rb
│ │ ├── data_type.rb
│ │ ├── download_status_type.rb
│ │ ├── download_type.rb
│ │ ├── feed_type.rb
│ │ ├── mutation_type.rb
│ │ ├── query_type.rb
│ │ ├── request_type.rb
│ │ └── source_type.rb
├── helpers
│ └── application_helper.rb
├── javascript
│ ├── components
│ │ ├── app.vue
│ │ ├── download_status_subscription.vue
│ │ ├── feeds
│ │ │ ├── add_request.vue
│ │ │ ├── bookmarklet.vue
│ │ │ ├── destroy.vue
│ │ │ ├── has_feed_mixin.js
│ │ │ ├── index.vue
│ │ │ ├── list.vue
│ │ │ ├── list_item.vue
│ │ │ ├── new.vue
│ │ │ ├── new_request_form.vue
│ │ │ └── settings.vue
│ │ ├── navbar.vue
│ │ ├── player.vue
│ │ ├── request.vue
│ │ ├── requests.vue
│ │ ├── requests
│ │ │ ├── change_feed.vue
│ │ │ └── destroy.vue
│ │ ├── sidebar.vue
│ │ ├── source.vue
│ │ ├── source_status.vue
│ │ ├── subscribe.vue
│ │ └── thumbnail.vue
│ ├── config.js
│ ├── error-toaster.js
│ ├── mutations.js
│ ├── packs
│ │ ├── application.js
│ │ └── main.js
│ ├── queries.js
│ ├── router.js
│ ├── store
│ │ ├── feeds.js
│ │ ├── index.js
│ │ └── requests.js
│ ├── stylesheets
│ │ ├── actions.scss
│ │ ├── application.scss
│ │ ├── authentication.scss
│ │ ├── bootstrap-customization.scss
│ │ ├── bootstrap-variables.scss
│ │ ├── feeds.scss
│ │ ├── hamburger.scss
│ │ ├── layout.scss
│ │ ├── navbar.scss
│ │ ├── player.scss
│ │ ├── sidebar.scss
│ │ ├── sources.scss
│ │ ├── subscribe.scss
│ │ └── thumbnail.scss
│ └── utils.js
├── jobs
│ ├── .keep
│ ├── downloader
│ │ └── fetch_source_job.rb
│ ├── garbage_collection_job.rb
│ └── upgrade_youtube_dl_job.rb
├── models
│ ├── application_model.rb
│ ├── concerns
│ │ └── .keep
│ ├── devise_user.rb
│ ├── download.rb
│ ├── feed.rb
│ ├── request.rb
│ ├── source.rb
│ └── user.rb
├── presenters
│ ├── application_presenter.rb
│ ├── download_presenter.rb
│ ├── download_status_presenter.rb
│ ├── feed_presenter.rb
│ ├── file_presenter.rb
│ ├── request_presenter.rb
│ └── source_presenter.rb
├── templates
│ ├── feeds
│ │ └── render.rss.builder
│ └── layouts
│ │ └── application.html.slim
└── views
│ ├── application_part.rb
│ ├── application_view.rb
│ ├── application_view_context.rb
│ ├── layouts
│ ├── _application.html.slim
│ ├── application.html.slim
│ └── devise.html.slim
│ ├── parts
│ └── source.rb
│ ├── views
│ └── feeds
│ │ └── render.rb
│ └── vue_apps
│ └── main.html.slim
├── babel.config.js
├── bin
├── bundle
├── podify
├── rails
├── rake
├── setup
├── start-server
├── start-worker
├── webpack
├── webpack-dev-server
└── yarn
├── config.ru
├── config
├── application.rb
├── boot.rb
├── cable.yml
├── credentials.yml.enc
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── application_controller_renderer.rb
│ ├── assets.rb
│ ├── backtrace_silencers.rb
│ ├── content_security_policy.rb
│ ├── cookies_serializer.rb
│ ├── devise.rb
│ ├── dry-view-reloading-patch.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ ├── new_framework_defaults_7_0.rb
│ ├── permissions_policy.rb
│ ├── sequel.rb
│ ├── shrine.rb
│ ├── system.rb
│ ├── wrap_parameters.rb
│ └── youtube_dl.rb
├── locales
│ ├── devise.en.yml
│ ├── en.yml
│ └── formats.en.yml
├── puma.rb
├── routes.rb
├── sidekiq.yml
├── spring.rb
├── system
│ ├── event_logger.rb
│ └── initial_user.rb
├── webpack
│ ├── development.js
│ ├── environment.js
│ ├── loaders
│ │ └── vue.js
│ ├── production.js
│ └── test.js
└── webpacker.yml
├── db
├── migrate
│ ├── 20200412191625_create_first_tables.rb
│ ├── 20200423145735_remove_title_from_sources.rb
│ ├── 20200425113118_create_users.rb
│ ├── 20200501124610_create_requests.rb
│ ├── 20200501175137_add_authentication_token_to_users.rb
│ ├── 20200508224831_create_feeds.rb
│ ├── 20200519161740_add_file_data_to_downloads.rb
│ ├── 20200522123751_remove_path_from_downloads.rb
│ ├── 20200522131956_add_feed_id_to_requests.rb
│ └── 20200523151156_add_position_to_feeds.rb
└── schema.rb
├── demo.gif
├── dev-env
├── docker-compose.yml
├── postgres
│ └── init
│ │ └── main.sh
├── tmuxp.yml
└── up
├── docker
├── build
├── docker-compose.build.yml
├── docker-compose.example.yml
├── entrypoint.sh
└── push
├── lib
├── application_contract.rb
├── assets
│ └── .keep
├── cli
│ ├── commands.rb
│ └── commands
│ │ ├── downloads
│ │ └── delete.rb
│ │ ├── sources
│ │ ├── add.rb
│ │ ├── delete.rb
│ │ ├── fetch.rb
│ │ ├── import.rb
│ │ ├── list.rb
│ │ ├── request.rb
│ │ └── selection.rb
│ │ └── user.rb
├── downloader
│ ├── fetch_source.rb
│ ├── fetcher_factory.rb
│ ├── fetchers
│ │ ├── file.rb
│ │ └── youtube_dl.rb
│ └── result.rb
├── downloads
│ ├── contract.rb
│ ├── create.rb
│ ├── destroy.rb
│ └── file_uploader.rb
├── events
│ ├── logger.rb
│ ├── logger
│ │ └── listener.rb
│ └── publisher.rb
├── expand_path.rb
├── feeds
│ ├── contract.rb
│ ├── create.rb
│ ├── destroy.rb
│ └── order.rb
├── requests
│ ├── contract.rb
│ ├── create.rb
│ ├── create_for_url.rb
│ ├── destroy.rb
│ └── update.rb
├── sources
│ ├── contract.rb
│ ├── create.rb
│ ├── destroy.rb
│ ├── find_or_create.rb
│ ├── import_directory.rb
│ └── import_file.rb
├── tasks
│ └── .keep
├── types.rb
└── users
│ ├── contract.rb
│ ├── create.rb
│ └── create_initial.rb
├── log
└── .keep
├── logo
├── attributions
├── favicon.svg
├── logo.svg
├── podcast-favicon.svg
└── podcast-solid.svg
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── 404.html
├── 422.html
├── 500.html
├── apple-touch-icon-precomposed.png
├── apple-touch-icon.png
├── favicon.ico
└── robots.txt
├── spec
├── factories
│ ├── feeds.rb
│ ├── sources.rb
│ └── users.rb
├── fixtures
│ └── files
│ │ └── example.mp4
├── integration
│ └── lib
│ │ ├── requests
│ │ └── create_for_url_spec.rb
│ │ └── sources
│ │ └── import_file_spec.rb
├── rails_helper.rb
└── spec_helper.rb
├── tmp
└── .keep
├── vendor
└── .keep
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | /vendor/bundle
3 | /.bundle
4 | /log
5 | /tmp
6 | /public/packs
7 | /.git*
8 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://podify@localhost/podify_development
2 | REDIS_URL=redis://localhost:6379
3 |
4 | HOST=podify.localhost
5 | PORT=3000
6 |
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://podify:FJJuo5onaohF8rp@localhost/podify_test
2 |
3 | HOST=localhost
4 | PORT=3000
5 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | tags:
8 | - 'v*'
9 |
10 | jobs:
11 | test:
12 | name: Test
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Check out repository
16 | uses: actions/checkout@v2
17 | - name: Set up Ruby
18 | uses: ruby/setup-ruby@v1
19 | with:
20 | bundler-cache: true
21 | - name: Run rspec
22 | run: |
23 | bundle exec sequel -m db/migrate $DATABASE_URL
24 | bundle exec rspec
25 | env:
26 | DATABASE_URL: postgres://postgres:pass@localhost:5432/podify
27 | REDIS_URL: redis://localhost:6379
28 |
29 | services:
30 | postgres:
31 | image: postgres
32 | env:
33 | POSTGRES_USER: postgres
34 | POSTGRES_PASSWORD: pass
35 | POSTGRES_DB: podify
36 | ports:
37 | - 5432:5432
38 | # needed because the postgres container does not provide a healthcheck
39 | # tmpfs makes DB faster by using RAM
40 | options: >-
41 | --mount type=tmpfs,destination=/var/lib/postgresql/data
42 | --health-cmd pg_isready
43 | --health-interval 10s
44 | --health-timeout 5s
45 | --health-retries 20
46 |
47 | redis:
48 | image: redis
49 | ports:
50 | - 6379:6379
51 |
52 | push_to_registry:
53 | name: Push Docker image to Docker Hub
54 | needs: test
55 | runs-on: ubuntu-latest
56 | steps:
57 | - name: Check out the repo
58 | uses: actions/checkout@v2
59 |
60 | - name: Log in to Docker Hub
61 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
62 | with:
63 | username: ${{ secrets.DOCKER_USERNAME }}
64 | password: ${{ secrets.DOCKER_PASSWORD }}
65 |
66 | # - name: Set up QEMU
67 | # uses: docker/setup-qemu-action@master
68 | # with:
69 | # platforms: all
70 |
71 | - name: Set up Docker Buildx
72 | id: buildx
73 | uses: docker/setup-buildx-action@master
74 |
75 | - name: Cache Docker layers
76 | uses: actions/cache@v2
77 | with:
78 | path: /tmp/.buildx-cache
79 | key: ${{ runner.os }}-buildx-${{ github.sha }}
80 | restore-keys: |
81 | ${{ runner.os }}-buildx-
82 |
83 | - name: Extract metadata (tags, labels) for Docker
84 | id: meta
85 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
86 | with:
87 | images: maxhollmann/podify
88 | tags: |
89 | type=ref,event=branch
90 | type=semver,pattern=v{{version}},event=tag
91 | type=semver,pattern=v{{major}},event=tag
92 | type=semver,pattern=v{{major}}.{{minor}},event=tag
93 | # type=ref,event=pr
94 |
95 | - name: Build and push Docker image
96 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
97 | with:
98 | builder: ${{ steps.buildx.outputs.name }}
99 | context: .
100 | push: true
101 | tags: ${{ steps.meta.outputs.tags }}
102 | labels: ${{ steps.meta.outputs.labels }}
103 | platforms: linux/amd64 # ,linux/arm64,linux/arm/v7
104 | cache-from: type=local,src=/tmp/.buildx-cache
105 | cache-to: type=local,dest=/tmp/.buildx-cache-new
106 |
107 | # This ugly bit is necessary if you don't want your cache to grow forever
108 | # till it hits GitHub's limit of 5GB.
109 | # Temp fix
110 | # https://github.com/docker/build-push-action/issues/252
111 | # https://github.com/moby/buildkit/issues/1896
112 | - name: Move cache
113 | run: |
114 | rm -rf /tmp/.buildx-cache
115 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache
116 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore all logfiles and tempfiles.
11 | /log/*
12 | /tmp/*
13 | !/log/.keep
14 | !/tmp/.keep
15 |
16 |
17 | /public/assets
18 | .byebug_history
19 |
20 | # Ignore master key for decrypting credentials and more.
21 | /config/master.key
22 |
23 | /public/packs
24 | /public/packs-test
25 | /node_modules
26 | /yarn-error.log
27 | yarn-debug.log*
28 | .yarn-integrity
29 | /.env.*.local
30 | /spec/examples.txt
31 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | ruby 3.2.1
2 | nodejs 12.22.12
3 | yarn 1.22.10
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:3.2.1-bullseye
2 |
3 | RUN apt-get update -qq && apt-get install -y nodejs npm postgresql-client python3 python3-pip ffmpeg
4 | RUN npm install -g yarn
5 |
6 | RUN mkdir /app
7 | WORKDIR /app
8 |
9 | COPY Gemfile /app/Gemfile
10 | COPY Gemfile.lock /app/Gemfile.lock
11 | RUN bundle config set clean true && \
12 | bundle config set deployment true && \
13 | bundle config set no-cache true && \
14 | bundle config set without 'development test' && \
15 | bundle install
16 |
17 | COPY package.json /app/package.json
18 | COPY yarn.lock /app/yarn.lock
19 | RUN yarn install --production
20 |
21 | COPY . /app
22 |
23 | RUN chmod +x /app/docker/entrypoint.sh
24 | ENTRYPOINT ["/app/docker/entrypoint.sh"]
25 | EXPOSE 3000
26 |
27 | ENV PATH="/app/bin:${PATH}"
28 | ENV RAILS_ENV=production
29 | ENV NODE_ENV=production
30 |
31 | # Precompile assets
32 | RUN webpack
33 |
34 | ENV RAILS_SERVE_STATIC_FILES="yes"
35 | ENV RAILS_LOG_TO_STDOUT="yes"
36 | ENV PORT=3000
37 |
38 | CMD ["start-server"]
39 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby '3.2.1'
5 |
6 | gem 'dotenv-rails', "~> 2.7", groups: [:development, :test], require: 'dotenv/rails-now'
7 |
8 | gem 'rails', '~> 7.0'
9 | # Use Puma as the app server
10 | gem 'puma', '~> 4.3'
11 | # Use SCSS for stylesheets
12 | gem 'sass-rails', '~> 6.0'
13 | # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
14 | gem 'webpacker', '~> 4.2'
15 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
16 | gem 'jbuilder', '~> 2.10'
17 | # Use Active Model has_secure_password
18 | # gem 'bcrypt', '~> 3.1.7'
19 |
20 | # prevents .../ruby/3.2.0/psych/visitors/to_ruby.rb:432:in `visit_Psych_Nodes_Alias': Alias parsing was not enabled. To enable it, pass `aliases: true` to `Psych::load` or `Psych::safe_load`. (Psych::AliasesNotEnabled)
21 | gem 'psych', '< 4'
22 |
23 | gem 'pg', "~> 1.2"
24 | gem 'sequel-rails', "~> 1.1"
25 |
26 | gem 'lograge', "~> 0.11"
27 | gem 'logstash-event', "~> 1.2"
28 | gem 'sentry-raven', "~> 3.0"
29 |
30 | gem 'sidekiq', "~> 6.0"
31 | gem 'sidekiq-scheduler', "~> 3.0"
32 |
33 | gem 'graphql', "~> 1.10"
34 | gem 'graphiql-rails', "~> 1.7"
35 |
36 | gem 'dry-rails', "~> 0.3.0"
37 | gem 'dry-monads', "~> 1.4.0"
38 | gem 'dry-view', "~> 0.7.0"
39 | gem 'dry-cli', "~> 0.7.0"
40 | gem 'dry-events', "~> 0.3.0"
41 | gem 'dry-effects', "~> 0.1.0"
42 | gem 'dry-container', "~> 0.8.0"
43 | gem 'dry-auto_inject', "~> 0.8.0"
44 |
45 | gem 'devise', "~> 4.7"
46 | gem 'sequel-devise', "~> 0.0"
47 | gem 'devise-bootstrap-views', '~> 1.1'
48 | gem 'devise-token_authenticatable', "~> 1.1"
49 |
50 | gem 'shrine', '~> 3.2'
51 |
52 | gem 'ytdl', "~> 0.2.0"
53 |
54 | gem 'slim-rails', "~> 3.2"
55 | gem 'builder', "~> 3.2"
56 | gem 'mimemagic', "~> 0.3"
57 |
58 | gem 'amazing_print', "~> 1.2"
59 |
60 | group :development, :test do
61 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
62 | gem 'byebug', "~> 11.1", platforms: [:mri, :mingw, :x64_mingw]
63 | gem 'pry-rails', "~> 0.3"
64 |
65 | gem 'rspec', "~> 3.9"
66 | gem 'rspec-rails', "~> 4.0"
67 | gem 'factory_bot_rails', "~> 5.2"
68 | end
69 |
70 | group :development do
71 | # Preview mail in the browser instead of sending
72 | gem 'letter_opener'
73 |
74 | # Debug I18n keys
75 | # gem 'i18n-debug'
76 |
77 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
78 | gem 'web-console'
79 | gem 'listen'
80 | gem 'better_errors'
81 | gem 'binding_of_caller'
82 | end
83 |
84 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
85 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Podify
2 |
3 | 
4 | 
5 | 
6 | 
7 |
8 | Create podcasts from anything youtube-dl can handle.
9 |
10 | 
11 |
12 | Podify is a self-hosted service that allows you to download videos and audio from any source supported by [youtube-dl](https://github.com/layer8x/youtube-dl.rb), sort the downloads into feeds, and subscribe to these feeds using your favorite podcast app (I recommend [Podcast Addict](https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en) if you're on Android).
13 |
14 | ## Installation using Docker
15 |
16 | Here's a [sample `docker-compose.yml`](docker/docker-compose.example.yml).
17 |
18 | ## Configuration via Environment Variables
19 |
20 | | Variable | Required | Description |
21 | |:----------------------|:---------|:------------------------------------------------------------------------------------------------------|
22 | | DATABASE_URL | Yes | `postgres://` url pointing to postgres database |
23 | | REDIS_URL | Yes | `redis://` url pointing to redis instance |
24 | | URL_HOST | Yes | Where the app will be available, e.g. `https://podify.yourdomain.com` |
25 | | SECRET_KEY_BASE | Yes | This is used to encrypt sessions, see [below](#secret_key_base) |
26 | | STORAGE_DIR | Yes | Where downloads will be stored |
27 | | INITIAL_USER_EMAIL | No | Email of user to create at startup |
28 | | INITIAL_USER_PASSWORD | No | Password of user to create at startup |
29 | | ENABLE_SIGNUP | No | Allow anyone with access to the site to sign up. `"yes"` or `"no"` (default: `"yes"`) |
30 |
31 | ### `SECRET_KEY_BASE`
32 |
33 | This is utilized to encrypt and sign sessions. It's recommended that you generate a new one instead of using the one from the example `docker-compose.yml`. You can generate a new one using
34 |
35 | docker run --rm maxhollmann/podify rails secret
36 |
37 | ## Development
38 |
39 | ```shell
40 | docker compose -f dev-env/docker-compose.yml up
41 | bundle install
42 | yarn install
43 |
44 | bundle exec sequel -m db/migrate postgres://podify@localhost/podify_development
45 | bundle exec sequel -m db/migrate postgres://podify@localhost/podify_test
46 |
47 | bin/webpack-dev-server
48 | bundle exec rails s
49 | bundle exec sidekiq
50 | ```
51 |
52 | If you have [tmux](https://github.com/tmux/tmux/wiki) and [tmuxp](https://tmuxp.git-pull.com/) installed you can start all required services by running `dev-env/up`.
53 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 |
3 | //= link graphiql/rails/application.css
4 | //= link graphiql/rails/application.js
5 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/assets/images/.keep
--------------------------------------------------------------------------------
/app/assets/images/favicon/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/assets/images/favicon/16.png
--------------------------------------------------------------------------------
/app/assets/images/favicon/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/assets/images/favicon/32.png
--------------------------------------------------------------------------------
/app/assets/images/logo/navbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/assets/images/logo/navbar.png
--------------------------------------------------------------------------------
/app/assets/images/logo/square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/assets/images/logo/square.png
--------------------------------------------------------------------------------
/app/assets/images/logo/wide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/assets/images/logo/wide.png
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | identified_by :current_user
4 |
5 | def connect
6 | self.current_user = helpers.current_user
7 | end
8 |
9 | private
10 |
11 | class Helpers
12 | extend Dry::Initializer
13 | option :request
14 | include Devise::Controllers::Helpers
15 | end
16 |
17 | def helpers
18 | @helpers ||= Helpers.new(request: request)
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/channels/broadcasts/application_broadcast.rb:
--------------------------------------------------------------------------------
1 | module Broadcasts
2 | class ApplicationBroadcast
3 | private
4 |
5 | def server
6 | ActionCable.server
7 | end
8 |
9 | def broadcast(channel, payload)
10 | server.broadcast(channel, payload)
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/channels/broadcasts/download_status.rb:
--------------------------------------------------------------------------------
1 | module Broadcasts
2 | class DownloadStatus < ApplicationBroadcast
3 | extend Dry::Initializer
4 | option :source
5 |
6 | def broadcast(status)
7 | super(channel, source_id: source.id, status: status)
8 | end
9 |
10 | def call
11 | broadcast(DownloadStatusPresenter.wrap(source).for_broadcast)
12 | end
13 |
14 | def progress(progress)
15 | broadcast({ status: 'downloading', progress: progress })
16 | end
17 |
18 | def downloaded
19 | broadcast({ status: 'downloaded' })
20 | end
21 |
22 | def channel
23 | "sources/#{source.id}/download_status"
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/channels/download_status_channel.rb:
--------------------------------------------------------------------------------
1 | class DownloadStatusChannel < ApplicationCable::Channel
2 | def subscribed
3 | # TODO: authorize with pandit
4 | if current_user.sources_dataset[params[:source]]
5 | stream_from "sources/#{params[:source]}/download_status"
6 | else
7 | reject
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include Devise::Controllers::Helpers
3 |
4 | # append_view_path "app/templates"
5 |
6 | skip_before_action :verify_authenticity_token
7 | before_action :set_raven_context
8 |
9 | if Rails.env.development?
10 | before_action do
11 | Dry::View.reset_cache!
12 | end
13 | end
14 |
15 | private
16 |
17 | ##### dry-view helpers
18 | def render_view(identifier, with: {}, **input, &block)
19 | container["views.#{identifier}"].call(
20 | context: dry_view_context(**with),
21 | **input,
22 | &block
23 | ).to_s.html_safe
24 | end
25 |
26 | # view_context is defined by ActionView
27 | def dry_view_context(**options)
28 | container["application_view_context"].with(**view_context_options(**options))
29 | end
30 |
31 | def view_context_options(**overrides)
32 | {
33 | request: request
34 | }.merge(overrides)
35 | end
36 |
37 | def container
38 | Podify::Container
39 | end
40 |
41 |
42 | def set_raven_context
43 | # Raven.user_context(id: session[:current_user_id])
44 | Raven.extra_context(params: params.to_unsafe_h, url: request.url)
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/app/controllers/application_devise_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationDeviseController < ApplicationController
2 | layout 'devise'
3 | end
4 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/feeds_controller.rb:
--------------------------------------------------------------------------------
1 | class FeedsController < ApplicationController
2 | schema(:show) do
3 | required(:token).filled(:string)
4 | end
5 |
6 | def show
7 | raise ActionController::RoutingError.new("Format not supported") unless request.format.rss?
8 |
9 | feed = Feed.with_token!(safe_params[:token])
10 | render xml: render_view('feeds.render', feed: feed)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/graphql_controller.rb:
--------------------------------------------------------------------------------
1 | class GraphQLController < ApplicationController
2 | # If accessing from outside this domain, nullify the session
3 | # This allows for outside API access while preventing CSRF attacks,
4 | # but you'll have to authenticate your user separately
5 | # protect_from_forgery with: :null_session
6 |
7 | def execute
8 | variables = ensure_hash(params[:variables])
9 | query = params[:query]
10 | operation_name = params[:operationName]
11 | context = {
12 | # Query context goes here, for example:
13 | current_user: current_user,
14 | }
15 | result = UserAPI::PodifySchema.execute(query, variables: variables, context: context, operation_name: operation_name)
16 | render json: result
17 | rescue => e
18 | raise e unless Rails.env.development?
19 | handle_error_in_development e
20 | end
21 |
22 | private
23 |
24 | # Handle form data, JSON body, or a blank value
25 | def ensure_hash(ambiguous_param)
26 | case ambiguous_param
27 | when String
28 | if ambiguous_param.present?
29 | ensure_hash(JSON.parse(ambiguous_param))
30 | else
31 | {}
32 | end
33 | when Hash, ActionController::Parameters
34 | ambiguous_param
35 | when nil
36 | {}
37 | else
38 | raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
39 | end
40 | end
41 |
42 | def handle_error_in_development(e)
43 | logger.error e.message
44 | logger.error e.backtrace.join("\n")
45 |
46 | render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/app/controllers/vue_apps_controller.rb:
--------------------------------------------------------------------------------
1 | class VueAppsController < ApplicationController
2 | before_action :authenticate_user!
3 |
4 | def main
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/graphql/user_api/mutations/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/graphql/user_api/mutations/.keep
--------------------------------------------------------------------------------
/app/graphql/user_api/mutations/base_mutation.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Mutations
3 | class BaseMutation < GraphQL::Schema::RelayClassicMutation
4 | argument_class Types::BaseArgument
5 | field_class Types::BaseField
6 | input_object_class Types::BaseInputObject
7 | object_class Types::BaseObject
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/graphql/user_api/mutations/create_feed.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | class Mutations::CreateFeed < Mutations::BaseMutation
3 | null true
4 |
5 | argument :name, String, required: true
6 |
7 | field :feed, Types::FeedType, null: true
8 | field :errors, [String], null: false
9 |
10 | include Dry::Monads[:result]
11 | include Podify::Import[
12 | create_feed: 'feeds.create',
13 | ]
14 |
15 | def resolve(name:)
16 | case create_feed.call(user_id: context[:current_user].id, name: name)
17 | in Success(feed)
18 | {
19 | feed: FeedPresenter.wrap(feed),
20 | errors: [],
21 | }
22 | in Failure(failure)
23 | {
24 | feed: nil,
25 | errors: [failure.to_s]
26 | }
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/graphql/user_api/mutations/destroy_feed.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | class Mutations::DestroyFeed < Mutations::BaseMutation
3 | null true
4 |
5 | argument :id, Integer, required: true
6 |
7 | field :errors, [String], null: false
8 |
9 | include Dry::Monads[:result]
10 | include Podify::Import[
11 | destroy_feed: 'feeds.destroy',
12 | ]
13 |
14 | def resolve(id:)
15 | feed = context[:current_user].feeds_dataset[id]
16 | return { errors: ['Feed does not exist'] } unless feed
17 |
18 | case destroy_feed.call(feed)
19 | in Success()
20 | {
21 | errors: [],
22 | }
23 | in Failure(failure)
24 | {
25 | errors: [failure.to_s]
26 | }
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/graphql/user_api/mutations/destroy_request.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | class Mutations::DestroyRequest < Mutations::BaseMutation
3 | null true
4 |
5 | argument :id, Integer, required: true
6 |
7 | field :errors, [String], null: false
8 |
9 | include Dry::Monads[:result]
10 | include Podify::Import[
11 | destroy_request: 'requests.destroy',
12 | ]
13 |
14 | def resolve(id:)
15 | request = context[:current_user].requests_dataset[id]
16 | return { errors: ['Request does not exist'] } unless request
17 |
18 | case destroy_request.call(request)
19 | in Success()
20 | {
21 | errors: [],
22 | }
23 | in Failure(failure)
24 | {
25 | errors: [failure.to_s]
26 | }
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/graphql/user_api/mutations/order_feeds.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | class Mutations::OrderFeeds < Mutations::BaseMutation
3 | null true
4 |
5 | argument :feed_ids, [Integer], required: true
6 |
7 | field :feeds, [Types::FeedType], null: false
8 |
9 | include Podify::Import[
10 | order_feeds: 'feeds.order',
11 | ]
12 |
13 | def resolve(feed_ids:)
14 | feeds = order_feeds.call(user: context[:current_user], feed_ids: feed_ids).value!
15 | { feeds: FeedPresenter.wrap_all(feeds) }
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/graphql/user_api/mutations/request_for_url.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | class Mutations::RequestForUrl < Mutations::BaseMutation
3 | null true
4 |
5 | argument :url, String, required: true
6 | argument :feed_id, Integer, required: true
7 |
8 | field :request, Types::RequestType, null: true
9 | field :errors, [String], null: false
10 |
11 | include Dry::Monads[:result]
12 | include Dry::Effects::Handler.Defer
13 | include Podify::Import[
14 | create_request_for_url: 'requests.create_for_url',
15 | ]
16 |
17 | def resolve(url:, feed_id:)
18 | feed = context[:current_user].feeds_dataset[feed_id]
19 | if feed.nil?
20 | return {
21 | request: nil,
22 | errors: ['Feed does not exist'],
23 | }
24 | end
25 |
26 | result = with_defer do
27 | create_request_for_url.call(user: context[:current_user], feed: feed, url: url)
28 | end
29 |
30 | case result
31 | in Success(request)
32 | {
33 | request: RequestPresenter.wrap(request),
34 | errors: [],
35 | }
36 | in Failure(failure)
37 | {
38 | request: nil,
39 | errors: [failure.to_s],
40 | }
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/graphql/user_api/mutations/update_request.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | class Mutations::UpdateRequest < Mutations::BaseMutation
3 | null true
4 |
5 | argument :id, Integer, required: true
6 | argument :feed_id, Integer, required: true
7 |
8 | field :errors, [String], null: false
9 | field :request, Types::RequestType, null: true
10 |
11 | include Dry::Monads[:result]
12 | include Podify::Import[
13 | update_request: 'requests.update',
14 | ]
15 |
16 | def resolve(id:, feed_id:)
17 | request = context[:current_user].requests_dataset[id]
18 | return { errors: ['Request does not exist'] } unless request
19 |
20 | case update_request.call(request, feed_id: feed_id)
21 | in Success()
22 | {
23 | errors: [],
24 | request: RequestPresenter.wrap(request),
25 | }
26 | in Failure(failure)
27 | {
28 | errors: [failure.to_s],
29 | }
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/graphql/user_api/podify_schema.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | class PodifySchema < GraphQL::Schema
3 | mutation(Types::MutationType)
4 | query(Types::QueryType)
5 |
6 | # Opt in to the new runtime (default in future graphql-ruby versions)
7 | use GraphQL::Execution::Interpreter
8 | use GraphQL::Analysis::AST
9 |
10 | # Add built-in connections for pagination
11 | use GraphQL::Pagination::Connections
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/app/graphql/user_api/types/.keep
--------------------------------------------------------------------------------
/app/graphql/user_api/types/base_argument.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class BaseArgument < GraphQL::Schema::Argument
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/base_enum.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class BaseEnum < GraphQL::Schema::Enum
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/base_field.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class BaseField < GraphQL::Schema::Field
4 | argument_class Types::BaseArgument
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/base_input_object.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class BaseInputObject < GraphQL::Schema::InputObject
4 | argument_class Types::BaseArgument
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/base_interface.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | module BaseInterface
4 | include GraphQL::Schema::Interface
5 |
6 | field_class Types::BaseField
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/base_object.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class BaseObject < GraphQL::Schema::Object
4 | field_class Types::BaseField
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/base_scalar.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class BaseScalar < GraphQL::Schema::Scalar
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/base_union.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class BaseUnion < GraphQL::Schema::Union
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/data_type.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class DataType < Types::BaseObject
4 | field :requests, [RequestType], null: false
5 | field :feeds, [FeedType], null: false
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/download_status_type.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class DownloadStatusType < Types::BaseObject
4 | field :status, String, null: false
5 | field :place_in_queue, String, null: true
6 | field :retry_status, String, null: true
7 | field :progress, Integer, null: true
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/download_type.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class DownloadType < Types::BaseObject
4 | field :id, Integer, null: false
5 | field :title, String, null: false
6 | field :author, String, null: true
7 | field :thumbnail_url, String, null: true
8 | field :format, String, null: true
9 | field :size, String, null: true, method: :human_size
10 | field :download_url, String, null: true
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/feed_type.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class FeedType < Types::BaseObject
4 | field :id, Integer, null: false
5 | field :token, String, null: false
6 | field :name, String, null: false
7 | field :type, String, null: false
8 | field :rss_url, String, null: false
9 | field :requests, [Types::RequestType], null: false
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/mutation_type.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class MutationType < Types::BaseObject
4 | field :request_for_url, mutation: Mutations::RequestForUrl
5 | field :update_request, mutation: Mutations::UpdateRequest
6 | field :destroy_request, mutation: Mutations::DestroyRequest
7 | field :create_feed, mutation: Mutations::CreateFeed
8 | field :destroy_feed, mutation: Mutations::DestroyFeed
9 | field :order_feeds, mutation: Mutations::OrderFeeds
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/query_type.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class QueryType < Types::BaseObject
4 | field :data, DataType, description: 'All requests of the current user', null: false
5 | def data
6 | OpenStruct.new(
7 | requests: RequestPresenter.wrap_all(
8 | context[:current_user].requests_dataset.order { created_at.desc }.all
9 | ),
10 | feeds: FeedPresenter.wrap_all(
11 | context[:current_user].feeds_dataset.ordered.all
12 | ),
13 | )
14 | end
15 |
16 | field :requests, [RequestType], description: 'All requests of the current user', null: false
17 | def requests
18 | RequestPresenter.wrap_all(
19 | context[:current_user].requests_dataset.order { created_at.desc }.all
20 | )
21 | end
22 |
23 | field :source, SourceType, description: 'A source requested by the user', null: false do
24 | argument :id, Integer, required: true
25 | end
26 | def source(id:)
27 | SourcePresenter.wrap(context[:current_user].sources_dataset.with_pk!(id))
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/request_type.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class RequestType < Types::BaseObject
4 | field :id, Integer, null: false
5 | field :source, Types::SourceType, null: true
6 | field :feed_id, Integer, null: false
7 | field :feed, Types::FeedType, null: false
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/graphql/user_api/types/source_type.rb:
--------------------------------------------------------------------------------
1 | module UserAPI
2 | module Types
3 | class SourceType < Types::BaseObject
4 | field :id, Integer, null: false
5 | field :url, String, null: false
6 | field :title, String, null: true
7 | field :author, String, null: true
8 | field :description, String, null: true
9 | field :thumbnail_url, String, null: true
10 |
11 | field :download_status, Types::DownloadStatusType, null: true
12 | field :last_download, Types::DownloadType, null: true
13 | field :downloads, [Types::DownloadType], null: false
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/javascript/components/app.vue:
--------------------------------------------------------------------------------
1 |
2 | Subscribe to this feed using your favorite podcast app: or click here.
5 |
11 |
4 |
13 |
9 | Bookmarklet
10 |
From #{link_to source.url, source.url}
36 |#{source.description}
37 | EOF 38 | end 39 | 40 | xml.itunes :summary, source.description 41 | 42 | xml.itunes :author, source.author 43 | xml.itunes :image, href: source.thumbnail_url 44 | # xml.itunes :duration, item.duration 45 | 46 | if source.last_download 47 | file = source.last_download.file 48 | xml.enclosure url: file.download_url, length: file.size, type: file.mime_type 49 | end 50 | end 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /app/templates/layouts/application.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Podify 5 | 6 | meta charset="utf-8" 7 | meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" 8 | meta name="viewport" content="width=device-width, initial-scale=1.0" 9 | 10 | = stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' 11 | = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' 12 | 13 | - %w[16 32].each do |size| 14 | = favicon_pack_tag "media/favicon/#{size}.png", rel: 'icon', sizes: "#{size}x#{size}" 15 | 16 | = yield :html_head 17 | 18 | body class=yield(:body_classes) 19 | == yield 20 | 21 | - if false && Rails.env.development? 22 | .container 23 | p= link_to 'Graphiql', graphiql_rails_path 24 | p= link_to 'Sidekiq', sidekiq_web_path 25 | -------------------------------------------------------------------------------- /app/views/application_part.rb: -------------------------------------------------------------------------------- 1 | class ApplicationPart < Dry::View::Part 2 | end 3 | -------------------------------------------------------------------------------- /app/views/application_view.rb: -------------------------------------------------------------------------------- 1 | class ApplicationView < Dry::View 2 | config.paths = Rails.root.join('app/templates') 3 | config.layout = "application" 4 | config.default_context = ApplicationViewContext.new 5 | config.part_namespace = Parts 6 | # config.scope_namespace = Scopes 7 | end 8 | -------------------------------------------------------------------------------- /app/views/application_view_context.rb: -------------------------------------------------------------------------------- 1 | require "dry/view/context" 2 | 3 | class ApplicationViewContext < Dry::View::Context 4 | include Devise::Controllers::Helpers 5 | 6 | def request 7 | _options[:request] 8 | end 9 | 10 | def method_missing(name, *args, &block) 11 | if url_helpers.respond_to?(name) 12 | url_helpers.public_send(name, *args, &block) 13 | elsif helpers.respond_to?(name) 14 | helpers.public_send(name, *args, &block) 15 | else 16 | super 17 | end 18 | end 19 | 20 | private 21 | 22 | def respond_to_missing?(name, include_private = false) 23 | url_helpers.respond_to?(name, include_private) || 24 | helpers.respond_to?(name, include_private) || 25 | super 26 | end 27 | 28 | def url_helpers 29 | @url_helpers ||= Rails.application.routes.url_helpers 30 | end 31 | 32 | def helpers 33 | @helpers ||= ApplicationController.helpers 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/views/layouts/_application.html.slim: -------------------------------------------------------------------------------- 1 | ../../templates/layouts/application.html.slim -------------------------------------------------------------------------------- /app/views/layouts/application.html.slim: -------------------------------------------------------------------------------- 1 | ../../templates/layouts/application.html.slim -------------------------------------------------------------------------------- /app/views/layouts/devise.html.slim: -------------------------------------------------------------------------------- 1 | = content_for :body_classes, 'body-authentication' 2 | 3 | = render layout: 'layouts/application' do 4 | .authentication 5 | .logo 6 | img src= asset_pack_path('media/logo/navbar.png') 7 | .form 8 | == yield 9 | -------------------------------------------------------------------------------- /app/views/parts/source.rb: -------------------------------------------------------------------------------- 1 | module Parts 2 | class Source < ApplicationPart 3 | def display_title 4 | downloads.last&.title || url 5 | end 6 | 7 | def render_form 8 | render :form, source: self 9 | end 10 | 11 | def render_list_entry 12 | render :list_entry, source: self 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/views/feeds/render.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | module Feeds 3 | class Render < ApplicationView 4 | config.template = "feeds/render" 5 | config.layout = nil 6 | config.default_format = :rss 7 | 8 | expose(:feed) { |feed:| FeedPresenter.wrap(feed) } 9 | expose(:sources) { |feed:| SourcePresenter.wrap_all(feed.requests.map(&:source)) } 10 | expose(:feed_url) { |feed:| Rails.application.routes.url_helpers.root_url + "/feeds/#{feed.id}" } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/vue_apps/main.html.slim: -------------------------------------------------------------------------------- 1 | - content_for :html_head do 2 | meta name="root-url" content=root_url 3 | meta name="api-url" content=api_url 4 | = action_cable_meta_tag 5 | = javascript_pack_tag 'main' 6 | = stylesheet_pack_tag 'main' 7 | 8 | = content_for :body_classes, 'body-app' 9 | 10 | #main 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | var validEnv = ['development', 'test', 'production'] 3 | var currentEnv = api.env() 4 | var isDevelopmentEnv = api.env('development') 5 | var isProductionEnv = api.env('production') 6 | var isTestEnv = api.env('test') 7 | 8 | if (!validEnv.includes(currentEnv)) { 9 | throw new Error( 10 | 'Please specify a valid `NODE_ENV` or ' + 11 | '`BABEL_ENV` environment variables. Valid values are "development", ' + 12 | '"test", and "production". Instead, received: ' + 13 | JSON.stringify(currentEnv) + 14 | '.' 15 | ) 16 | } 17 | 18 | return { 19 | presets: [ 20 | isTestEnv && [ 21 | '@babel/preset-env', 22 | { 23 | targets: { 24 | node: 'current' 25 | } 26 | } 27 | ], 28 | (isProductionEnv || isDevelopmentEnv) && [ 29 | '@babel/preset-env', 30 | { 31 | forceAllTransforms: true, 32 | useBuiltIns: 'entry', 33 | corejs: 3, 34 | modules: false, 35 | exclude: ['transform-typeof-symbol'] 36 | } 37 | ] 38 | ].filter(Boolean), 39 | plugins: [ 40 | 'babel-plugin-macros', 41 | '@babel/plugin-syntax-dynamic-import', 42 | isTestEnv && 'babel-plugin-dynamic-import-node', 43 | '@babel/plugin-transform-destructuring', 44 | [ 45 | '@babel/plugin-proposal-class-properties', 46 | { 47 | loose: true 48 | } 49 | ], 50 | [ 51 | '@babel/plugin-proposal-object-rest-spread', 52 | { 53 | useBuiltIns: true 54 | } 55 | ], 56 | [ 57 | '@babel/plugin-transform-runtime', 58 | { 59 | helpers: false, 60 | regenerator: true, 61 | corejs: false 62 | } 63 | ], 64 | [ 65 | '@babel/plugin-transform-regenerator', 66 | { 67 | async: false 68 | } 69 | ] 70 | ].filter(Boolean) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= 65 | env_var_version || cli_arg_version || 66 | lockfile_version 67 | end 68 | 69 | def bundler_requirement 70 | return "#{Gem::Requirement.default}.a" unless bundler_version 71 | 72 | bundler_gem_version = Gem::Version.new(bundler_version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/podify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../config/boot' 4 | require_relative '../config/environment' 5 | #require 'cli/commands' 6 | 7 | Dry::CLI.new(CLI::Commands).call 8 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | puts "\n== Removing old logs and tempfiles ==" 21 | system! "bin/rails log:clear tmp:clear" 22 | 23 | puts "\n== Restarting application server ==" 24 | system! "bin/rails restart" 25 | end 26 | -------------------------------------------------------------------------------- /bin/start-server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | bundle exec rails server -b 0.0.0.0 5 | -------------------------------------------------------------------------------- /bin/start-worker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | bundle exec sequel -m db/migrate "$DATABASE_URL" 5 | pip3 install --upgrade yt-dlp 6 | 7 | bundle exec sidekiq -c 3 8 | -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | # require "active_job/railtie" 7 | # require "active_record/railtie" 8 | # require "active_storage/engine" 9 | require "action_controller/railtie" 10 | require "action_mailer/railtie" 11 | # require "action_mailbox/engine" 12 | # require "action_text/engine" 13 | require "action_view/railtie" 14 | require "action_cable/engine" 15 | # require "rails/test_unit/railtie" 16 | 17 | # Require the gems listed in Gemfile, including any gems 18 | # you've limited to :test, :development, or :production. 19 | Bundler.require(*Rails.groups) 20 | 21 | module Podify 22 | class Application < Rails::Application 23 | config.load_defaults 7.0 24 | 25 | # Don't generate system test files. 26 | config.generators.system_tests = nil 27 | 28 | config.autoload_paths << Rails.root.join('app/views') 29 | 30 | # Remove heinous monkey patch 31 | Dry::View::Part.undef_method :to_param 32 | 33 | # config.sequel.skip_connect = true 34 | Sequel.extension(:pg_json_ops) 35 | 36 | config.sequel.after_connect = proc do |conn| 37 | Sequel::Model.plugin :timestamps, update_on_create: true 38 | Sequel::Model.plugin :association_dependencies 39 | Sequel::Model.plugin :tactical_eager_loading 40 | 41 | Sequel::Model.db.extension :pg_array 42 | Sequel::Model.db.extension :pg_json 43 | Sequel::Model.db.extension :pg_enum 44 | end 45 | 46 | config.action_cable.mount_path = '/api/cable' 47 | 48 | if ENV['URL_HOST'] 49 | Rails.application.routes.default_url_options[:host] = ENV['URL_HOST'] 50 | config.action_controller.asset_host = ENV['URL_HOST'] 51 | elsif ENV['HOST'] 52 | host = "http#{'s' if ENV['SSL']}://#{ENV.fetch('HOST')}:#{ENV.fetch('PORT')}" 53 | Rails.application.routes.default_url_options[:host] = host 54 | config.action_controller.asset_host = host 55 | config.hosts << ENV.fetch('HOST') 56 | end 57 | 58 | if ENV['EXTRA_HOSTS'] 59 | config.hosts += ENV['EXTRA_HOSTS'].split(',') 60 | end 61 | 62 | if ENV['FIX_DOCKER_LOGS'] 63 | log_out = IO.new(IO.sysopen("/proc/1/fd/1", "w"),"w") 64 | log_out.sync = true 65 | config.logger = Logger.new(log_out) 66 | end 67 | 68 | Raven.configure do |config| 69 | config.dsn = ENV['SENTRY_DSN'] 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: redis 3 | url: "<%= ENV['REDIS_URL'] %>" 4 | channel_prefix: podify_development 5 | 6 | production: 7 | adapter: redis 8 | url: "<%= ENV['REDIS_URL'] %>" 9 | channel_prefix: podify_production 10 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | Ns+vgheSqeBzCkJEn3JaLcANplCbS4BKtTcwehf9lA650jBJEPjYiDyz1BlBbnWO6+sqp7uhfFjrqU/7d4IOGOyH73X2CbbSGY93ioujyCMEy6yfkQi1//H3olRjf3OuoFgeyircvP3A6RXd5ac9nm6S2AONqgpvvG5XXSmxD9tpmYNtL8L5Pf06XelPUcDxSTD+3ceK0MxMf7EZHlNG3L2mjEYAP/duxKt74R3vj69/NKcCA5ynbKrJlnJe2w4jPTlwV/yWNPscprB17qo4SnmXyjew57ltSGXAFh+zkVaeFqaQW2T628NlWqROpigycKBqKUKCH0Ipm3rfI/NxINyvEdQlfIYvKsYcTLb0IWeBNlFI0WsDrU6aIPBptQMAsniPTtkakCHrAV1sW44K4y7Pn6fagBt8npB8--EpgPmI4ESLbWQ887--34nkmlk31W2bpPQg6KHJOQ== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | _default: &default 2 | adapter: postgres 3 | encoding: unicode 4 | pool: 5 5 | url: "<%= ENV['DATABASE_URL'] %>" 6 | 7 | development: 8 | <<: *default 9 | 10 | test: 11 | <<: *default 12 | 13 | production: 14 | <<: *default 15 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.action_controller.perform_caching = true 24 | config.action_controller.enable_fragment_cache_logging = true 25 | 26 | config.cache_store = :memory_store 27 | config.public_file_server.headers = { 28 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 29 | } 30 | else 31 | config.action_controller.perform_caching = false 32 | 33 | config.cache_store = :null_store 34 | end 35 | 36 | # Don't care if the mailer can't send. 37 | config.action_mailer.raise_delivery_errors = false 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise exceptions for disallowed deprecations. 45 | config.active_support.disallowed_deprecation = :raise 46 | 47 | # Tell Active Support which deprecation messages to disallow. 48 | config.active_support.disallowed_deprecation_warnings = [] 49 | 50 | # Suppress logger output for asset requests. 51 | config.assets.quiet = true 52 | 53 | # Raises error for missing translations. 54 | # config.i18n.raise_on_missing_translations = true 55 | 56 | # Annotate rendered view with file names. 57 | # config.action_view.annotate_rendered_view_with_filenames = true 58 | 59 | config.action_mailer.delivery_method = :letter_opener 60 | config.action_mailer.perform_deliveries = true 61 | 62 | # Uncomment if you wish to allow Action Cable access from any origin. 63 | # config.action_cable.disable_request_forgery_protection = true 64 | end 65 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 12 | config.cache_classes = true 13 | 14 | # Eager loading loads your whole application. When running a single test locally, 15 | # this probably isn't necessary. It's a good idea to do in a continuous integration 16 | # system, or in some way before deploying your code. 17 | config.eager_load = ENV["CI"].present? 18 | 19 | # Configure public file server for tests with Cache-Control for performance. 20 | config.public_file_server.enabled = true 21 | config.public_file_server.headers = { 22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 23 | } 24 | 25 | # Show full error reports and disable caching. 26 | config.consider_all_requests_local = true 27 | config.action_controller.perform_caching = false 28 | config.cache_store = :null_store 29 | 30 | # Raise exceptions instead of rendering exception templates. 31 | config.action_dispatch.show_exceptions = false 32 | 33 | # Disable request forgery protection in test environment. 34 | config.action_controller.allow_forgery_protection = false 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Tell Action Mailer not to deliver emails to the real world. 39 | # The :test delivery method accumulates sent emails in the 40 | # ActionMailer::Base.deliveries array. 41 | config.action_mailer.delivery_method = :test 42 | 43 | # Print deprecation notices to the stderr. 44 | config.active_support.deprecation = :stderr 45 | 46 | # Raise exceptions for disallowed deprecations. 47 | config.active_support.disallowed_deprecation = :raise 48 | 49 | # Tell Active Support which deprecation messages to disallow. 50 | config.active_support.disallowed_deprecation_warnings = [] 51 | 52 | # Raises error for missing translations. 53 | # config.i18n.raise_on_missing_translations = true 54 | 55 | # Annotate rendered view with file names. 56 | # config.action_view.annotate_rendered_view_with_filenames = true 57 | end 58 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap and inline scripts 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/dry-view-reloading-patch.rb: -------------------------------------------------------------------------------- 1 | # TODO: only in development 2 | 3 | require 'dry/view' 4 | require 'dry/view/renderer' 5 | require 'dry/view/path' 6 | require 'dry/view/scope_builder' 7 | require 'dry/view/part_builder' 8 | require 'dry/view/tilt' 9 | 10 | module Dry 11 | class View 12 | def self.reset_cache! 13 | [self, Renderer, Path, ScopeBuilder, PartBuilder, Tilt].each do |klass| 14 | klass.instance_variable_set(:@__cache__, ::Concurrent::Map.new) 15 | end 16 | nil 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 5 | # notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | 18 | ActiveSupport::Inflector.inflections(:en) do |inflect| 19 | inflect.acronym 'GraphQL' 20 | inflect.acronym 'API' 21 | end 22 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /config/initializers/sequel.rb: -------------------------------------------------------------------------------- 1 | DB = Sequel::Model.db 2 | -------------------------------------------------------------------------------- /config/initializers/shrine.rb: -------------------------------------------------------------------------------- 1 | require 'shrine/storage/file_system' 2 | 3 | Shrine.storages = { 4 | store: Shrine::Storage::FileSystem.new(ENV.fetch('STORAGE_DIR', 'tmp/storage')), 5 | cache: Shrine::Storage::FileSystem.new(ENV.fetch('CACHE_STORAGE_DIR', 'tmp/storage/cache')), 6 | } 7 | 8 | Shrine.plugin :sequel 9 | Shrine.plugin :upload_options, cache: { move: true }, store: { move: true } 10 | Shrine.plugin :cached_attachment_data 11 | Shrine.plugin :restore_cached_data 12 | -------------------------------------------------------------------------------- /config/initializers/system.rb: -------------------------------------------------------------------------------- 1 | Dry::Rails.container do 2 | # cherry-pick features 3 | config.auto_inject_constant = 'Import' 4 | config.features = %i[application_contract safe_params controller_helpers] 5 | 6 | register('events', memoize: true) do 7 | require 'events/publisher' 8 | Events::Publisher.new 9 | end 10 | 11 | register('logger', memoize: true) do 12 | if Rails.env.development? || Rails.env.test? 13 | Logger.new( 14 | STDOUT, 15 | formatter: proc { |severity, datetime, progname, msg| 16 | date_format = datetime.strftime("%Y-%m-%d %H:%M:%S") 17 | out = "#{date_format.blue}" 18 | out += " #{severity.red}" 19 | out += " (#{progname})" if progname 20 | out += " #{msg}\n" 21 | out 22 | } 23 | ) 24 | else 25 | Logger.new(STDOUT) 26 | end 27 | end 28 | 29 | register('downloader.result') do 30 | require 'downloader/result' 31 | Downloader::Result 32 | end 33 | 34 | register('action_cable.server') do 35 | ActionCable.server 36 | end 37 | 38 | register('redis', memoize: true) do 39 | Redis.new 40 | end 41 | 42 | auto_register!('lib') 43 | auto_register!('app/views') 44 | auto_register!('app/jobs') do |config| 45 | config.memoize = true 46 | config.instance { |component| component.loader.constant } 47 | end 48 | 49 | setting :env, ENV 50 | boot(:settings, from: :system) do 51 | settings do 52 | key :storage_dir, Types::Coercible::Pathname.constrained(filled: true).default(Rails.root.join('tmp', 'storage').freeze) 53 | 54 | key :initial_user_email, Types::String.optional.default(nil) 55 | key :initial_user_password, Types::String.optional.default(nil) 56 | 57 | key :enable_signup, Types::Coercible::YesNoBoolean.optional.default(true) 58 | end 59 | end 60 | end 61 | 62 | Dry::System.register_provider( 63 | :podify, 64 | boot_path: Rails.root.join('config', 'system').realpath 65 | ) 66 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/youtube_dl.rb: -------------------------------------------------------------------------------- 1 | YoutubeDL::Command.config.executable = 'yt-dlp' 2 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | time: 35 | formats: 36 | rss: "%a, %d %b %Y %H:%M:%S %Z %:z" 37 | -------------------------------------------------------------------------------- /config/locales/formats.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | time: 3 | formats: 4 | time: "%l:%M%P UTC" 5 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 12 | # 13 | port ENV.fetch("PORT") { 3000 } 14 | 15 | # Specifies the `environment` that Puma will run in. 16 | # 17 | environment ENV.fetch("RAILS_ENV") { "development" } 18 | 19 | # Specifies the `pidfile` that Puma will use. 20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 21 | 22 | # Specifies the number of `workers` to boot in clustered mode. 23 | # Workers are forked web server processes. If using threads and workers together 24 | # the concurrency of the application would be max `threads` * `workers`. 25 | # Workers do not work on JRuby or Windows (both of which do not support 26 | # processes). 27 | # 28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 29 | 30 | # Use the `preload_app!` method when specifying a `workers` number. 31 | # This directive tells Puma to first boot the application and load code 32 | # before forking the application. This takes advantage of Copy On Write 33 | # process behavior so workers use less memory. 34 | # 35 | # preload_app! 36 | 37 | # Allow puma to be restarted by `rails restart` command. 38 | plugin :tmp_restart 39 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq/web' 2 | 3 | Rails.application.routes.draw do 4 | if Rails.env.development? 5 | mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/api" 6 | end 7 | post "/api", to: "graphql#execute" 8 | 9 | root to: 'vue_apps#main' 10 | devise_for :user, class_name: 'DeviseUser' 11 | 12 | get 'rss/:token', to: 'feeds#show', as: :feed, constraints: { format: :rss } 13 | 14 | mount Downloads::FileUploader.download_endpoint => '/download' 15 | mount Sidekiq::Web => '/sidekiq' 16 | 17 | get '*path', to: 'vue_apps#main' 18 | end 19 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | :schedule: 2 | garbage_collect: 3 | cron: '* * * * *' 4 | class: GarbageCollectionJob 5 | 6 | upgrade_youtube_dl: 7 | cron: '6 * * * *' 8 | class: UpgradeYoutubeDlJob 9 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /config/system/event_logger.rb: -------------------------------------------------------------------------------- 1 | Dry::Rails.container do 2 | boot(:event_logger) do 3 | start do 4 | Podify::Container['events.logger'].subscribe 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/system/initial_user.rb: -------------------------------------------------------------------------------- 1 | Dry::Rails.container do 2 | boot(:initial_user) do |container| 3 | start do 4 | Podify::Container['users.create_initial'].call.or do |error| 5 | raise error.to_s 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | const vue = require('./loaders/vue') 4 | 5 | environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin()) 6 | environment.loaders.prepend('vue', vue) 7 | module.exports = environment 8 | -------------------------------------------------------------------------------- /config/webpack/loaders/vue.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: /\.vue(\.erb)?$/, 3 | use: [{ 4 | loader: 'vue-loader' 5 | }] 6 | } 7 | -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpacker.yml: -------------------------------------------------------------------------------- 1 | # Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | default: &default 4 | source_path: app/javascript 5 | source_entry_path: packs 6 | public_root_path: public 7 | public_output_path: packs 8 | cache_path: tmp/cache/webpacker 9 | check_yarn_integrity: false 10 | webpack_compile_output: true 11 | 12 | # Additional paths webpack should lookup modules 13 | # ['app/assets', 'engine/foo/app/assets'] 14 | resolved_paths: [] 15 | 16 | # Reload manifest.json on all requests so we reload latest compiled packs 17 | cache_manifest: false 18 | 19 | # Extract and emit a css file 20 | extract_css: false 21 | 22 | static_assets_extensions: 23 | - .jpg 24 | - .jpeg 25 | - .png 26 | - .gif 27 | - .tiff 28 | - .ico 29 | - .svg 30 | - .eot 31 | - .otf 32 | - .ttf 33 | - .woff 34 | - .woff2 35 | 36 | extensions: 37 | - .vue 38 | - .mjs 39 | - .js 40 | - .sass 41 | - .scss 42 | - .css 43 | - .module.sass 44 | - .module.scss 45 | - .module.css 46 | - .png 47 | - .svg 48 | - .gif 49 | - .jpeg 50 | - .jpg 51 | 52 | development: 53 | <<: *default 54 | compile: true 55 | 56 | # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules 57 | check_yarn_integrity: true 58 | 59 | # Reference: https://webpack.js.org/configuration/dev-server/ 60 | dev_server: 61 | https: false 62 | host: localhost 63 | port: 3035 64 | public: localhost:3035 65 | hmr: false 66 | # Inline should be set to true if using HMR 67 | inline: true 68 | overlay: true 69 | compress: true 70 | disable_host_check: true 71 | use_local_ip: false 72 | quiet: false 73 | pretty: false 74 | headers: 75 | 'Access-Control-Allow-Origin': '*' 76 | watch_options: 77 | ignored: '**/node_modules/**' 78 | 79 | 80 | test: 81 | <<: *default 82 | compile: true 83 | 84 | # Compile test packs to a separate directory 85 | public_output_path: packs-test 86 | 87 | production: 88 | <<: *default 89 | 90 | # Production depends on precompilation of packs prior to booting for performance. 91 | compile: false 92 | 93 | # Extract and emit a css file 94 | extract_css: true 95 | 96 | # Cache manifest.json for performance 97 | cache_manifest: true 98 | -------------------------------------------------------------------------------- /db/migrate/20200412191625_create_first_tables.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | 4 | create_table :sources do 5 | primary_key :id 6 | 7 | String :url, null: false, unique: true 8 | String :title 9 | DateTime :downloaded_at 10 | 11 | DateTime :created_at, null: false 12 | DateTime :updated_at, null: false 13 | end 14 | 15 | create_table :downloads do 16 | primary_key :id 17 | 18 | foreign_key :source_id, :sources, null: false 19 | String :path, null: true, unique: true 20 | String :fetcher, null: false 21 | String :author 22 | String :title 23 | String :thumbnail_url 24 | column :fetcher_information, :jsonb, null: false 25 | 26 | DateTime :created_at, null: false 27 | DateTime :updated_at, null: false 28 | end 29 | 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /db/migrate/20200423145735_remove_title_from_sources.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | up do 3 | alter_table :sources do 4 | drop_column :title 5 | end 6 | end 7 | 8 | down do 9 | alter_table :sources do 10 | add_column :title, String 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20200425113118_create_users.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | 4 | create_table :users do 5 | primary_key :id 6 | 7 | String :email, :null => false, :default => "" 8 | String :encrypted_password, :null => false, :default => "" 9 | 10 | ## Recoverable 11 | String :reset_password_token 12 | String :reset_password_sent_at 13 | 14 | ## Rememberable 15 | DateTime :remember_created_at 16 | 17 | ## Trackable 18 | Integer :sign_in_count, :default => 0, :null => false 19 | DateTime :current_sign_in_at 20 | DateTime :last_sign_in_at 21 | String :current_sign_in_ip 22 | String :last_sign_in_ip 23 | 24 | ## Confirmable 25 | String :confirmation_token 26 | DateTime :confirmed_at 27 | DateTime :confirmation_sent_at 28 | String :unconfirmed_email # Only if using reconfirmable 29 | 30 | ## Lockable 31 | # Integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts 32 | # String :unlock_token # Only if unlock strategy is :email or :both 33 | # DateTime :locked_at 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /db/migrate/20200501124610_create_requests.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | 4 | create_table :requests do 5 | primary_key :id 6 | foreign_key :user_id, :users, null: false 7 | foreign_key :source_id, :sources, null: false 8 | 9 | DateTime :created_at, null: false 10 | DateTime :updated_at, null: false 11 | 12 | index [:user_id, :source_id], unique: true 13 | end 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20200501175137_add_authentication_token_to_users.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | alter_table :users do 4 | add_column :authentication_token, :text, unique: true 5 | add_column :authentication_token_created_at, DateTime 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20200508224831_create_feeds.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | 4 | create_table :feeds do 5 | primary_key :id 6 | 7 | foreign_key :user_id, :users, null: false 8 | String :type, null: false, default: 'all' 9 | String :name, null: false 10 | String :token, null: false, unique: true 11 | 12 | DateTime :created_at, null: false 13 | DateTime :updated_at, null: false 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20200519161740_add_file_data_to_downloads.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | add_column :downloads, :file_data, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200522123751_remove_path_from_downloads.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | up do 3 | alter_table :downloads do 4 | drop_column :path 5 | end 6 | end 7 | 8 | down do 9 | alter_table :downloads do 10 | add_column :path, String, null: true, unique: true 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20200522131956_add_feed_id_to_requests.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | alter_table :requests do 4 | add_foreign_key :feed_id, :feeds 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20200523151156_add_position_to_feeds.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | alter_table :feeds do 4 | add_column :position, Integer, null: false, default: 9999999 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/demo.gif -------------------------------------------------------------------------------- /dev-env/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: postgres:12.3 6 | environment: 7 | POSTGRES_USER: postgres 8 | POSTGRES_PASSWORD: "" 9 | POSTGRES_HOST_AUTH_METHOD: trust 10 | volumes: 11 | - ./postgres/init:/docker-entrypoint-initdb.d 12 | ports: 13 | - "5432:5432" 14 | 15 | redis: 16 | image: redis:6 17 | ports: 18 | - "6379:6379" 19 | 20 | # memcached: 21 | # image: memcached 22 | # network_mode: host 23 | # ports: 24 | # - "11211:11211" 25 | -------------------------------------------------------------------------------- /dev-env/postgres/init/main.sh: -------------------------------------------------------------------------------- 1 | psql -U postgres -c "CREATE USER podify;" 2 | 3 | createdb -U postgres -O podify podify_development 4 | createdb -U postgres -O podify podify_test 5 | -------------------------------------------------------------------------------- /dev-env/tmuxp.yml: -------------------------------------------------------------------------------- 1 | session_name: podify 2 | windows: 3 | - window_name: all-processes 4 | layout: tiled 5 | panes: 6 | - shell_command: 7 | - tmux set -gw pane-border-status top 8 | 9 | - printf '\033]2;%s\033\\' 'Docker services' 10 | - cd dev-env 11 | - docker compose up 12 | 13 | - shell_command: 14 | - tmux set -gw pane-border-status top 15 | - printf '\033]2;%s\033\\' 'Server' 16 | - bundle exec rails s 17 | 18 | - shell_command: 19 | - tmux set -gw pane-border-status top 20 | - printf '\033]2;%s\033\\' 'Worker' 21 | - bundle exec sidekiq 22 | 23 | - shell_command: 24 | - tmux set -gw pane-border-status top 25 | - printf '\033]2;%s\033\\' 'webpack-dev-server' 26 | - bin/webpack-dev-server 27 | -------------------------------------------------------------------------------- /dev-env/up: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | 5 | pushd . > /dev/null 6 | cd "$SCRIPT_DIR/.." 7 | 8 | tmuxp load dev-env/tmuxp.yml 9 | 10 | popd > /dev/null 11 | -------------------------------------------------------------------------------- /docker/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker compose -f docker/docker-compose.build.yml build 4 | -------------------------------------------------------------------------------- /docker/docker-compose.build.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | podify: 5 | image: maxhollmann/podify:dev 6 | build: 7 | context: .. 8 | -------------------------------------------------------------------------------- /docker/docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | x-app-defaults: &app-defaults 4 | restart: always 5 | environment: &app-env 6 | URL_HOST: https://podify.yourdomain.com 7 | DATABASE_URL: postgres://podify:verysecurepassword@db/podify 8 | REDIS_URL: redis://redis 9 | SECRET_KEY_BASE: a57d57661ef5df58b46fab6f04304e89108f22f89b31d2242b31891102da87d519a1f3c6459c1d2716b3b8c5438ef43e06ed4c29c8fb059eb650dc2ec0062d57 10 | STORAGE_DIR: /storage 11 | INITIAL_USER_EMAIL: you@example.com 12 | INITIAL_USER_PASSWORD: yourpassword 13 | ENABLE_SIGNUP: "no" 14 | 15 | volumes: 16 | - storage:/storage 17 | 18 | depends_on: 19 | - db 20 | - redis 21 | 22 | services: 23 | web: 24 | <<: *app-defaults 25 | image: maxhollmann/podify:latest 26 | command: start-server 27 | ports: 28 | - 3000:3000 29 | environment: 30 | <<: *app-env 31 | 32 | worker: 33 | <<: *app-defaults 34 | image: maxhollmann/podify:latest 35 | command: start-worker 36 | environment: 37 | <<: *app-env 38 | 39 | db: 40 | image: postgres:12.3 41 | restart: always 42 | environment: 43 | POSTGRES_USER: podify 44 | POSTGRES_PASSWORD: verysecurepassword 45 | PGDATA: /var/lib/postgresql/data/pgdata 46 | volumes: 47 | - pgdata:/var/lib/postgresql/data/pgdata 48 | 49 | redis: 50 | image: redis:6 51 | restart: always 52 | 53 | volumes: 54 | pgdata: 55 | storage: 56 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Remove a potentially pre-existing server.pid for Rails. 5 | rm -f /app/tmp/pids/server.pid 6 | 7 | exec "$@" 8 | -------------------------------------------------------------------------------- /docker/push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$1" = "-h" ]]; then 4 | echo "Usage: docker-push [tag]" 5 | echo 6 | echo " tag defaults to 'dev'" 7 | exit 1 8 | fi 9 | 10 | tag=${1:-dev} 11 | 12 | image=maxhollmann/podify 13 | 14 | echo "Tagging $image:dev as $image:$tag" 15 | docker tag $image:dev $image:$tag 16 | 17 | echo "Pushing $image:$tag" 18 | docker push $image:$tag 19 | -------------------------------------------------------------------------------- /lib/application_contract.rb: -------------------------------------------------------------------------------- 1 | require 'dry/validation' 2 | require 'dry/monads' 3 | 4 | Dry::Validation.load_extensions(:monads) 5 | 6 | class ApplicationContract < Dry::Validation::Contract 7 | config.messages.top_namespace = :contracts 8 | config.messages.backend = :i18n 9 | config.messages.load_paths += Dir[Rails.root.join("config/locales/*.yml")] 10 | end 11 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/lib/assets/.keep -------------------------------------------------------------------------------- /lib/cli/commands.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | require 'cli/commands/downloads/delete' 3 | require 'cli/commands/sources/list' 4 | require 'cli/commands/sources/add' 5 | require 'cli/commands/sources/import' 6 | require 'cli/commands/sources/fetch' 7 | require 'cli/commands/sources/delete' 8 | require 'cli/commands/sources/request' 9 | 10 | module CLI 11 | module Commands 12 | extend Dry::CLI::Registry 13 | 14 | register "downloads", aliases: ["d"] do |prefix| 15 | prefix.register "delete", Downloads::Delete, aliases: ['d'] 16 | end 17 | 18 | register "sources", aliases: ["s"] do |prefix| 19 | prefix.register "list", Sources::List, aliases: ['l'] 20 | prefix.register "add", Sources::Add, aliases: ['a'] 21 | prefix.register "import", Sources::Import, aliases: ['i'] 22 | prefix.register "fetch", Sources::Fetch, aliases: ['f'] 23 | prefix.register "delete", Sources::Delete, aliases: ['d'] 24 | prefix.register "request", Sources::Request, aliases: ['r'] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/cli/commands/downloads/delete.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | 3 | module CLI 4 | module Commands 5 | module Downloads 6 | class Delete < Dry::CLI::Command 7 | desc "Delete downloads" 8 | 9 | include Podify::Import[ 10 | 'events', 11 | 'downloads.destroy' 12 | ] 13 | 14 | option :all, type: :boolean, default: false, desc: 'Delete all downloads' 15 | 16 | def call(all:, args: []) 17 | subscribe_to_events 18 | 19 | if all 20 | Download.each do |download| 21 | destroy.call(download) 22 | end 23 | else 24 | puts "Don't know which downloads to delete" 25 | end 26 | end 27 | 28 | def subscribe_to_events 29 | events.subscribe('downloads.destroyed') do |event| 30 | puts "Deleted #{event[:download].path}" 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/cli/commands/sources/add.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | 3 | module CLI 4 | module Commands 5 | module Sources 6 | class Add < Dry::CLI::Command 7 | desc "Add a source" 8 | 9 | include Dry::Monads[:result] 10 | 11 | include Podify::Import[ 12 | 'sources.create' 13 | ] 14 | 15 | option :url, type: :string, required: true, desc: 'URL of the media source' 16 | 17 | def call(url:) 18 | case create.call(url: url) 19 | in Success(source) 20 | puts "Source created, id: #{source.id}" 21 | in Failure(failure) 22 | puts "Failed:" 23 | ap failure 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/cli/commands/sources/delete.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | require 'cli/commands/sources/selection' 3 | 4 | module CLI 5 | module Commands 6 | module Sources 7 | class Delete < Dry::CLI::Command 8 | desc "Delete sources" 9 | 10 | include Podify::Import[ 11 | 'events', 12 | 'sources.destroy' 13 | ] 14 | 15 | include Selection 16 | 17 | def call(**args) 18 | subscribe_to_events 19 | 20 | selected_sources(args).each do |source| 21 | destroy.call(source) 22 | end 23 | end 24 | 25 | def subscribe_to_events 26 | events.subscribe('sources.destroyed') do |event| 27 | puts "Deleted source #{event[:source].url}" 28 | end 29 | events.subscribe('downloads.destroyed') do |event| 30 | puts "Deleted download #{event[:download].path}" 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/cli/commands/sources/fetch.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | require 'cli/commands/sources/selection' 3 | 4 | module CLI 5 | module Commands 6 | module Sources 7 | class Fetch < Dry::CLI::Command 8 | include Dry::Monads[:result] 9 | 10 | include Podify::Import[ 11 | 'events', 12 | 'downloader.fetch_source' 13 | ] 14 | 15 | include Selection 16 | 17 | desc "Fetch sources" 18 | 19 | def call(**args) 20 | selected_sources(args).each do |source| 21 | puts "Downloading source #{source.id}: '#{source.url}'" 22 | case fetch_source.call(source) 23 | in Success(download) 24 | puts "Downloaded to '#{download.path}'" 25 | in Failure(error, {output:}) 26 | puts "Failed with #{error}. Output:\n\n#{output}" 27 | in failure 28 | puts "Unknown failure: #{failure}" 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/cli/commands/sources/import.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | 3 | module CLI 4 | module Commands 5 | module Sources 6 | class Import < Dry::CLI::Command 7 | desc "Import existing files or directories" 8 | 9 | include Podify::Import[ 10 | 'events', 11 | 'sources.import_file', 12 | 'sources.import_directory' 13 | ] 14 | 15 | def call(args: []) 16 | subscribe_to_events 17 | 18 | args.each do |arg| 19 | path = Pathname(arg) 20 | if path.file? 21 | import = import_file 22 | elsif path.directory? 23 | import = import_directory 24 | else 25 | puts "#{path} does not exist" 26 | next 27 | end 28 | 29 | puts "Importing #{path}..." 30 | import.call(path) 31 | end 32 | 33 | puts 34 | puts "Imported #{@imported} files" 35 | end 36 | 37 | def subscribe_to_events 38 | @imported = 0 39 | 40 | events.subscribe('sources.created') do |event| 41 | puts "Imported \"#{event[:source].url}\" from #{event[:source].url}" 42 | @imported += 1 43 | end 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/cli/commands/sources/list.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | require 'cli/commands/sources/selection' 3 | 4 | module CLI 5 | module Commands 6 | module Sources 7 | class List < Dry::CLI::Command 8 | desc "List sources" 9 | 10 | include Selection 11 | 12 | def call(**args) 13 | selected_sources(args).each do |source| 14 | print "#{source.id}: #{source.url}" 15 | download = source.downloads.last 16 | print ": '#{download.path}'" if download 17 | puts 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/cli/commands/sources/request.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | require 'cli/commands/sources/selection' 3 | require 'cli/commands/user' 4 | 5 | module CLI 6 | module Commands 7 | module Sources 8 | class Request < Dry::CLI::Command 9 | desc "Create requests for sources from a user" 10 | 11 | include Dry::Monads[:result] 12 | 13 | include Podify::Import[ 14 | create_request: 'requests.create', 15 | ] 16 | 17 | include Selection 18 | include User 19 | 20 | def call(**args) 21 | user = selected_user(args) 22 | selected_sources(args).each do |source| 23 | case create_request.call(user_id: user.id, source_id: source.id) 24 | in Success(request) 25 | puts "#{request.source.url} requested" 26 | in Failure('requests.create.exists') 27 | puts "Already requested #{source.url}" 28 | in Failure(failure) 29 | puts "Failed requesting #{source.url}: #{failure}" 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/cli/commands/sources/selection.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | 3 | module CLI 4 | module Commands 5 | module Sources 6 | module Selection 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | option :all, type: :boolean, default: false, desc: 'All sources' 11 | option :id, type: :integer, default: nil, desc: '' 12 | end 13 | 14 | def selected_sources(args) 15 | if !args[:all] && args[:id].nil? 16 | puts 'Don\'t know which sources to select.' 17 | return [] 18 | end 19 | 20 | sources = Source.dataset 21 | sources = sources.where(id: args[:id]) if args[:id] 22 | 23 | sources.to_a.tap do |sources| 24 | if sources.none? 25 | puts 'No sources found' 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/cli/commands/user.rb: -------------------------------------------------------------------------------- 1 | require 'dry/cli' 2 | 3 | module CLI 4 | module Commands 5 | module User 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | option :email, type: :string, desc: 'Email of the user' 10 | end 11 | 12 | def selected_user(args) 13 | email = args.fetch(:email) 14 | 15 | ::User.by_email(email).tap do |user| 16 | unless user 17 | puts "Can't find user with email #{email}" 18 | exit 1 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/downloader/fetch_source.rb: -------------------------------------------------------------------------------- 1 | module Downloader 2 | class FetchSource 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'downloader.fetcher_factory', 7 | 'events', 8 | create_download: 'downloads.create', 9 | ] 10 | 11 | include Dry::Effects.Resolve(progress_callback: 'downloader.fetch_source.progress_callback') 12 | 13 | def call(source) 14 | progress_callback.start_download 15 | 16 | fetcher = yield fetcher_factory.call(source) 17 | result = yield fetcher.call(source) 18 | events.publish('downloader.fetch_source.fetched', result: result) 19 | download = yield create_download_from_result(result: result, source: source) 20 | 21 | progress_callback.complete 22 | 23 | Success(download) 24 | end 25 | 26 | def create_download_from_result(result:, source:) 27 | download = yield create_download.call(**result.to_h, path: result[:path].to_s, source_id: source.id) 28 | Success(download) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/downloader/fetcher_factory.rb: -------------------------------------------------------------------------------- 1 | module Downloader 2 | class FetcherFactory 3 | include Dry::Monads[:result] 4 | 5 | include Podify::Import[ 6 | 'downloader.fetchers.file', 7 | 'downloader.fetchers.youtube_dl', 8 | ] 9 | 10 | def call(source) 11 | case source.url 12 | when %r{\Afile://}i then Success(file) 13 | when %r{\Ahttps?://}i then Success(youtube_dl) 14 | else Failure('downloader.fetcher_factory.no_fetcher_for_scheme') 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/downloader/fetchers/file.rb: -------------------------------------------------------------------------------- 1 | module Downloader 2 | module Fetchers 3 | class File 4 | include Dry::Monads[:result, :do] 5 | 6 | include Podify::Import['downloader.result'] 7 | 8 | def call(source) 9 | path = yield get_path(source.url) 10 | Success(create_result(path)) 11 | end 12 | 13 | def get_path(url) 14 | if url =~ %r{\Afile://}i 15 | Success(Pathname(url.sub(%r{\Afile://}i, ''))) 16 | else 17 | Failure('downloader.fetchers.file.not_a_file_url') 18 | end 19 | end 20 | 21 | def create_result(path) 22 | result.new( 23 | fetcher: 'file', 24 | path: path, 25 | title: path.basename.sub_ext('').to_s, 26 | fetcher_information: {}, 27 | ) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/downloader/fetchers/youtube_dl.rb: -------------------------------------------------------------------------------- 1 | module Downloader 2 | module Fetchers 3 | class YoutubeDl 4 | include Dry::Monads[:result, :do] 5 | 6 | include Podify::Import[ 7 | 'settings', 8 | 'downloader.result', 9 | ] 10 | 11 | include Dry::Effects.Resolve(progress_callback: 'downloader.fetch_source.progress_callback') 12 | 13 | def call(source) 14 | download_state = yield download(source) 15 | Success(create_result(download_state)) 16 | end 17 | 18 | def download(source) 19 | YoutubeDL.download(source.url, **arguments(source)) 20 | .on_progress do |state:, **| 21 | progress_callback.progress(state.progress) 22 | end 23 | .on_complete do |state:, **| 24 | return Success(state) 25 | end 26 | .on_error do |state:, **| 27 | return handle_youtube_dl_error(state.error) 28 | end 29 | .call 30 | 31 | raise 'YoutubeDL finished without error but not complete either' 32 | end 33 | 34 | def arguments(source) 35 | { 36 | format: 'mp4,mp3', 37 | output: output_template(source), 38 | } 39 | end 40 | 41 | def output_template(source) 42 | (storage_dir(source) / '%(title)s.%(ext)s').to_s 43 | end 44 | 45 | def storage_dir(source) 46 | dir = settings.storage_dir / source.id.to_s.rjust(10, '0') 47 | dir.mkpath 48 | dir 49 | end 50 | 51 | def handle_youtube_dl_error(error) 52 | match = error.match(/STDERR:\n\n(.*)/m) 53 | if match 54 | handle_command_error(match[1]) 55 | else 56 | raise "Unexpected error: #{error}" unless match 57 | end 58 | end 59 | 60 | def handle_command_error(output) 61 | case output 62 | when /Unsupported URL:/ 63 | Failure(['downloads.fetcher.unsupported', output: output]) 64 | else 65 | Failure(['downloads.fetcher.unknown_error', output: output]) 66 | end 67 | end 68 | 69 | def create_result(download_state) 70 | result.new(**{ 71 | fetcher: 'youtube_dl', 72 | path: download_state.destination, 73 | author: download_state.info['uploader'], 74 | title: download_state.info['title'], 75 | thumbnail_url: download_state.info['thumbnail'], 76 | fetcher_information: download_state.info, 77 | }.compact) 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/downloader/result.rb: -------------------------------------------------------------------------------- 1 | module Downloader 2 | class Result < Dry::Struct 3 | attribute :fetcher, Types::String 4 | attribute :path, Types::Pathname 5 | attribute :author, Types::String.meta(omittable: true) 6 | attribute :title, Types::String.meta(omittable: true) 7 | attribute :thumbnail_url, Types::String.meta(omittable: true) 8 | attribute :fetcher_information, Types::Hash 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/downloads/contract.rb: -------------------------------------------------------------------------------- 1 | module Downloads 2 | class Contract < ApplicationContract 3 | params do 4 | required(:path).filled(:string) 5 | required(:source_id).value(:integer) 6 | required(:fetcher).filled(:string) 7 | required(:fetcher_information).value(:hash) 8 | optional(:author).value(:string) 9 | optional(:title).value(:string) 10 | optional(:thumbnail_url).value(:string) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/downloads/create.rb: -------------------------------------------------------------------------------- 1 | module Downloads 2 | class Create 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | 'downloads.contract' 8 | ] 9 | 10 | def call(attrs) 11 | attrs = attrs.dup 12 | 13 | attrs = yield validate(attrs) 14 | file = yield get_file(attrs.delete(:path)) 15 | download = yield create_download(attrs: attrs, file: file) 16 | events.publish('downloads.created', download: download) 17 | Success(download) 18 | end 19 | 20 | def validate(attrs) 21 | Success(yield(contract.call(attrs).to_monad).to_h) 22 | end 23 | 24 | def get_file(path) 25 | pathname = Pathname(path) 26 | return Failure(:not_a_file) unless pathname.file? 27 | 28 | Success(pathname.open('r')) 29 | end 30 | 31 | def ensure_unique_path(path) 32 | if Download.by_path(path).any? 33 | Failure('downloads.create.path_exists') 34 | else 35 | Success(true) 36 | end 37 | end 38 | 39 | def create_download(attrs:, file:) 40 | Success(Download.create(attrs.merge(file: file))) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/downloads/destroy.rb: -------------------------------------------------------------------------------- 1 | module Downloads 2 | class Destroy 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | ] 8 | 9 | def call(download) 10 | download.destroy 11 | events.publish('downloads.destroyed', download: download) 12 | Success() 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/downloads/file_uploader.rb: -------------------------------------------------------------------------------- 1 | module Downloads 2 | class FileUploader < Shrine 3 | plugin :download_endpoint, prefix: 'download', 4 | host: Rails.application.routes.default_url_options[:host], 5 | disposition: 'attachment' 6 | plugin :determine_mime_type, analyzer: :mimemagic 7 | 8 | def generate_location(io, record: nil, **context) 9 | [ 10 | 'downloads', 11 | sanitize_filename(record.author.presence || 'no_author'), 12 | sanitize_filename(context[:metadata]['filename']), 13 | ].join('/') 14 | end 15 | 16 | private 17 | 18 | def sanitize_filename(name) 19 | name.strip.gsub(/[^0-9A-Za-z.\-]+/, '_') 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/events/logger.rb: -------------------------------------------------------------------------------- 1 | module Events 2 | class Logger 3 | include Podify::Import['events', 'logger'] 4 | 5 | def subscribe 6 | events.subscribe self 7 | end 8 | 9 | def unsubscribe 10 | events.unsubscribe self 11 | end 12 | 13 | def respond_to_missing?(method, *) 14 | method.to_s.start_with?('on_') 15 | end 16 | 17 | private 18 | 19 | def method_missing(method, event) 20 | log_event(event) 21 | end 22 | 23 | def log_event(event) 24 | logger.debug event: { 25 | id: event.id, 26 | payload: clean_payload(event.payload), 27 | } 28 | end 29 | 30 | def clean_payload(payload) 31 | return payload unless payload.is_a?(Hash) 32 | 33 | payload.to_h do |k, v| 34 | v = v.id if v.is_a?(Sequel::Model) 35 | v = clean_payload(v) 36 | 37 | [k, v] 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/events/logger/listener.rb: -------------------------------------------------------------------------------- 1 | module Events 2 | class Logger 3 | class Listener 4 | include Podify::Import['events.logger'] 5 | 6 | def respond_to_missing?(method, *) 7 | method.to_s.start_with?('on_') 8 | end 9 | 10 | def method_missing(method, event) 11 | logger.log_event(event) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/events/publisher.rb: -------------------------------------------------------------------------------- 1 | module Events 2 | class Publisher 3 | include Dry::Events::Publisher[:podify] 4 | 5 | register_event('feeds.created') 6 | 7 | register_event('users.created') 8 | 9 | register_event('requests.created') 10 | register_event('requests.updated') 11 | register_event('requests.destroyed') 12 | 13 | register_event('sources.created') 14 | register_event('sources.destroyed') 15 | 16 | register_event('downloads.created') 17 | register_event('downloads.destroyed') 18 | 19 | register_event('downloader.fetch_source.fetched') 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/expand_path.rb: -------------------------------------------------------------------------------- 1 | class ExpandPath 2 | include Dry::Monads[:result, :do] 3 | include Dry::Monads::Do.for(:call) 4 | 5 | def call(path) 6 | path = Pathname(path) 7 | Success(path.realpath) 8 | rescue 9 | Failure(:does_not_exist) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/feeds/contract.rb: -------------------------------------------------------------------------------- 1 | module Feeds 2 | class Contract < ApplicationContract 3 | params do 4 | required(:name).filled(:string) 5 | required(:user_id).value(:integer) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/feeds/create.rb: -------------------------------------------------------------------------------- 1 | module Feeds 2 | class Create 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | 'feeds.contract', 8 | ] 9 | 10 | def call(attrs) 11 | attrs = yield(contract.call(attrs).to_monad).to_h 12 | feed = yield create_feed(attrs) 13 | events.publish('feeds.created', feed: feed) 14 | Success(feed) 15 | end 16 | 17 | def create_feed(attrs) 18 | Success(Feed.create(**attrs, type: 'manual')) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/feeds/destroy.rb: -------------------------------------------------------------------------------- 1 | module Feeds 2 | class Destroy 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | destroy_request: 'requests.destroy', 8 | ] 9 | 10 | def call(feed) 11 | yield ensure_may_destroy(feed) 12 | DB.transaction do 13 | yield destroy_requests(feed.requests) 14 | feed.destroy 15 | end 16 | events.publish('feeds.destroyed', feed: feed) 17 | Success() 18 | end 19 | 20 | def ensure_may_destroy(feed) 21 | if feed.type == 'all' 22 | Failure('feeds.destroy.special_feed') 23 | else 24 | Success() 25 | end 26 | end 27 | 28 | def destroy_requests(requests) 29 | requests.each do |request| 30 | yield destroy_request.call(request) 31 | end 32 | Success() 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/feeds/order.rb: -------------------------------------------------------------------------------- 1 | module Feeds 2 | class Order 3 | include Dry::Monads[:result, :do] 4 | 5 | def call(user:, feed_ids:) 6 | feeds = get_ordered_feeds(dataset: user.feeds_dataset, feed_ids: feed_ids) 7 | update_positions(feeds) 8 | Success(feeds) 9 | end 10 | 11 | def get_ordered_feeds(dataset:, feed_ids:) 12 | feeds_by_id = dataset 13 | .where(id: feed_ids) 14 | .all 15 | .group_by(&:id) 16 | feed_ids.flat_map { |id| feeds_by_id[id] || [] } 17 | end 18 | 19 | def update_positions(feeds) 20 | feeds.each_with_index do |feed, index| 21 | feed.update(position: index) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/requests/contract.rb: -------------------------------------------------------------------------------- 1 | module Requests 2 | class Contract < ApplicationContract 3 | params do 4 | required(:user_id).value(:integer) 5 | required(:source_id).value(:integer) 6 | required(:feed_id).value(:integer) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/requests/create.rb: -------------------------------------------------------------------------------- 1 | module Requests 2 | class Create 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | 'requests.contract', 8 | ] 9 | 10 | def call(attrs) 11 | attrs = yield(contract.call(attrs).to_monad).to_h 12 | yield ensure_uniqueness(attrs) 13 | request = yield create_request(attrs) 14 | events.publish('requests.created', request: request) 15 | Success(request) 16 | end 17 | 18 | def ensure_uniqueness(attrs) 19 | return Failure('requests.create.exists') if Request.where(attrs).count > 0 20 | Success() 21 | end 22 | 23 | def create_request(attrs) 24 | Success(Request.create(attrs)) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/requests/create_for_url.rb: -------------------------------------------------------------------------------- 1 | module Requests 2 | class CreateForUrl 3 | include Dry::Monads[:result, :do, :maybe] 4 | 5 | include Podify::Import[ 6 | create_request: 'requests.create', 7 | find_or_create_source: 'sources.find_or_create', 8 | ] 9 | 10 | def call(user:, feed:, url:) 11 | DB.transaction do 12 | source = yield find_or_create_source.call(url) 13 | request = yield create_request.call(user_id: user.id, source_id: source.id, feed_id: feed.id) 14 | Success(request) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/requests/destroy.rb: -------------------------------------------------------------------------------- 1 | module Requests 2 | class Destroy 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | ] 8 | 9 | def call(request) 10 | request.destroy 11 | events.publish('requests.destroyed', request: request) 12 | Success() 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/requests/update.rb: -------------------------------------------------------------------------------- 1 | module Requests 2 | class Update 3 | include Dry::Monads[:result, :maybe, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | 'requests.contract', 8 | ] 9 | 10 | def call(request, feed_id:) 11 | feed = yield get_feed(request, feed_id) 12 | request.update(feed_id: feed.id) 13 | events.publish('requests.updated', request: request) 14 | Success() 15 | end 16 | 17 | def get_feed(request, feed_id) 18 | Maybe(request.user.feeds_dataset[feed_id]).or( 19 | Failure('requests.update.feed_not_found') 20 | ) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/sources/contract.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | class Contract < ApplicationContract 3 | params do 4 | required(:url).filled(:string) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/sources/create.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | class Create 3 | include Dry::Monads[:result, :do] 4 | include Dry::Effects.Defer 5 | 6 | include Podify::Import[ 7 | 'events', 8 | 'sources.contract', 9 | 'downloader.fetch_source_job', 10 | ] 11 | 12 | def call(attrs) 13 | attrs = yield(contract.call(attrs).to_monad).to_h 14 | source = yield create_source(attrs) 15 | events.publish('sources.created', source: source) 16 | later { fetch_source_job.perform_async(source.id) } # to get out of any transactions before the job runs 17 | Success(source) 18 | end 19 | 20 | def create_source(attrs) 21 | Success(Source.create(attrs)) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/sources/destroy.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | class Destroy 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | destroy_download: 'downloads.destroy', 8 | destroy_request: 'requests.destroy', 9 | ] 10 | 11 | def call(source) 12 | DB.transaction do 13 | yield destroy_requests(source.requests) 14 | yield destroy_downloads(source.downloads) 15 | source.destroy 16 | end 17 | events.publish('sources.destroyed', source: source) 18 | Success() 19 | end 20 | 21 | def destroy_downloads(downloads) 22 | downloads.each do |download| 23 | yield destroy_download.call(download) 24 | end 25 | Success() 26 | end 27 | 28 | def destroy_requests(requests) 29 | requests.each do |request| 30 | yield destroy_request.call(request) 31 | end 32 | Success() 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/sources/find_or_create.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | class FindOrCreate 3 | include Dry::Monads[:result, :do, :maybe] 4 | 5 | include Podify::Import[ 6 | 'sources.create', 7 | ] 8 | 9 | def call(url) 10 | find(url).or do 11 | source = yield create.call(url: url) 12 | Success(source) 13 | end 14 | end 15 | 16 | def find(url) 17 | Maybe(Source.by_url(url).first) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/sources/import_directory.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | class ImportDirectory 3 | include Dry::Monads[:result, :do] 4 | include Dry::Monads::Do.for(:call) 5 | 6 | include Podify::Import['expand_path', 'sources.import_file'] 7 | 8 | def call(path) 9 | path = yield expand_path.call(path) 10 | yield assert_directory(path) 11 | 12 | Success( 13 | all_files(path).map do |file| 14 | import_file.call(file) 15 | end.to_a 16 | ) 17 | end 18 | 19 | def assert_directory(path) 20 | if path.directory? 21 | Success() 22 | else 23 | Failure(:not_a_directory) 24 | end 25 | end 26 | 27 | def all_files(path) 28 | path.find.lazy.select(&:file?) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/sources/import_file.rb: -------------------------------------------------------------------------------- 1 | module Sources 2 | class ImportFile 3 | include Dry::Monads[:result, :do, :maybe] 4 | include Dry::Effects::Handler.Defer 5 | 6 | include Podify::Import[ 7 | 'expand_path', 8 | find_or_create_source: 'sources.find_or_create', 9 | ] 10 | 11 | def call(path) 12 | with_defer do 13 | DB.transaction do 14 | path = yield expand_path.call(path) 15 | yield assert_file(path) 16 | source = yield get_source(path) 17 | next Success(source) 18 | end 19 | end 20 | end 21 | 22 | def assert_file(path) 23 | return Failure(:not_a_file) unless path.file? 24 | Success() 25 | end 26 | 27 | def get_source(path) 28 | source = yield find_or_create_source.call("file://#{path}") 29 | Success(source) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/types.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | include Dry.Types() 3 | 4 | Pathname = Types.Instance(Pathname) 5 | 6 | module Coercible 7 | Pathname = Types::Pathname.constructor { |path| Pathname(path) } 8 | 9 | YesNoBoolean = Types.Constructor(TrueClass) do |input| 10 | next input if input === true || input === false 11 | 12 | case input.to_s.downcase 13 | when 'yes', 'y', 'true' 14 | true 15 | when 'no', 'n', 'false' 16 | false 17 | else 18 | raise ArgumentError, "Invalid input to Types::Coercible::YesNoBoolean: #{input.inspect}" 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/users/contract.rb: -------------------------------------------------------------------------------- 1 | module Users 2 | class Contract < ApplicationContract 3 | params do 4 | required(:email).filled(:string) 5 | required(:password).filled(:string) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/users/create.rb: -------------------------------------------------------------------------------- 1 | module Users 2 | class Create 3 | include Dry::Monads[:result, :do] 4 | 5 | include Podify::Import[ 6 | 'events', 7 | 'users.contract', 8 | ] 9 | 10 | def call(attrs) 11 | attrs = yield(contract.call(attrs).to_monad).to_h 12 | user = yield create_user(attrs) 13 | events.publish('users.created', user: user) 14 | Success(user) 15 | end 16 | 17 | def create_user(attrs) 18 | Success(DeviseUser.create(attrs)) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/users/create_initial.rb: -------------------------------------------------------------------------------- 1 | module Users 2 | class CreateInitial 3 | include Dry::Monads[:result, :do, :maybe] 4 | 5 | include Podify::Import[ 6 | 'users.create', 7 | 'settings', 8 | ] 9 | 10 | def call 11 | params = { 12 | email: settings.initial_user_email, 13 | password: settings.initial_user_password, 14 | } 15 | 16 | return Success(nil) if params.compact.empty? 17 | 18 | User.db.transaction do 19 | User.dataset.lock('EXCLUSIVE') 20 | 21 | Maybe(User.by_email(settings.initial_user_email)).fmap do |existing| 22 | return Success(existing) 23 | end 24 | 25 | create.call(params).fmap do |user| 26 | return Success(user) 27 | end.or do |failure| 28 | return Failure("Failed creating the initial user: #{failure.errors.to_h.map { |*a| a.join(' ') }.join(', ')}") 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podify-org/podify/b656a910b25b94948581edd15d29bf9ff36ad64e/log/.keep -------------------------------------------------------------------------------- /logo/attributions: -------------------------------------------------------------------------------- 1 | Podcast icon: Font Awesome 2 | -------------------------------------------------------------------------------- /logo/podcast-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "podify", 3 | "private": true, 4 | "dependencies": { 5 | "@chenfengyuan/vue-qrcode": "^1.0.2", 6 | "@fortawesome/fontawesome-free": "^5.15.1", 7 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 8 | "@fortawesome/free-brands-svg-icons": "^5.15.1", 9 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 10 | "@fortawesome/vue-fontawesome": "^0.1.10", 11 | "@rails/ujs": "^6.0.3", 12 | "@rails/webpacker": "4.2.2", 13 | "actioncable-vue": "^2.3.2", 14 | "apollo-cache-inmemory": "^1.6.6", 15 | "apollo-client": "^2.6.10", 16 | "apollo-link-http": "^1.5.17", 17 | "bootstrap": "4.4.1", 18 | "bootstrap-vue": "^2.20.1", 19 | "es6-promise": "^4.2.8", 20 | "graphql": "^15.4.0", 21 | "graphql-tag": "^2.11.0", 22 | "hamburgers": "^1.1.3", 23 | "jquery": "~3.4.1", 24 | "plyr": "^3.6.3", 25 | "popper.js": "^1.16.1", 26 | "vue": "^2.6.12", 27 | "vue-apollo": "^3.0.5", 28 | "vue-clipboard2": "^0.3.1", 29 | "vue-loader": "^15.9.5", 30 | "vue-loading-overlay": "^3.4.2", 31 | "vue-router": "^3.5.0", 32 | "vue-template-compiler": "^2.6.12", 33 | "vuedraggable": "^2.24.3", 34 | "vuex": "^3.6.0" 35 | }, 36 | "version": "0.1.0", 37 | "devDependencies": { 38 | "webpack-dev-server": "^3.11.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |