{"id":28099387,"url":"https://github.com/paulappz/pauloluyege.com","last_synced_at":"2026-04-16T08:36:41.369Z","repository":{"id":144614826,"uuid":"99761028","full_name":"paulappz/pauloluyege.com","owner":"paulappz","description":"My Personal Website Source Code","archived":false,"fork":false,"pushed_at":"2024-11-11T06:41:41.000Z","size":2006,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-11-11T07:26:01.962Z","etag":null,"topics":["bootstrap","css3","html5","javascript"],"latest_commit_sha":null,"homepage":"http://pauloluyege.com","language":"CSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/paulappz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-08-09T03:25:00.000Z","updated_at":"2024-11-11T06:41:45.000Z","dependencies_parsed_at":null,"dependency_job_id":"545d9348-bf6b-4df6-8f5b-08dcd02e2a65","html_url":"https://github.com/paulappz/pauloluyege.com","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paulappz%2Fpauloluyege.com","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paulappz%2Fpauloluyege.com/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paulappz%2Fpauloluyege.com/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paulappz%2Fpauloluyege.com/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/paulappz","download_url":"https://codeload.github.com/paulappz/pauloluyege.com/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254000631,"owners_count":21997437,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bootstrap","css3","html5","javascript"],"created_at":"2025-05-13T18:00:57.669Z","updated_at":"2026-04-16T08:36:41.336Z","avatar_url":"https://github.com/paulappz.png","language":"CSS","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n---\ndisplayed_sidebar: null\ntitle:  YouTube Playlist \u0026 Videos Catalog in Port\ndescription: Guild to help you process YouTube playlist and video data into Port.\n---\n\nimport Tabs from \"@theme/Tabs\"\nimport TabItem from \"@theme/TabItem\"\nimport PortTooltip from \"/src/components/tooltip/tooltip.jsx\"\n\n# YouTube Playlist \u0026 Videos Catalog in Port\n\nThis guide will help you set up an automated process to catalog YouTube playlist and video data into Port. Using Port's GitHub action, you’ll fetch YouTube data and ingest it into Port for easy tracking and visualization.\n\n## Prerequisites\n\n1. [Create a Port account](https://app.getport.io) and set up API credentials.\n2. [Obtain a YouTube Data API Key](https://console.cloud.google.com/apis/credentials).\n3. [Set up GitHub secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in your repository for:\n   - `YOUTUBE_API_KEY`: Your YouTube API key.\n   - `CLIENT_ID`: Your Port client ID.\n   - `CLIENT_SECRET`: Your Port client secret.\n\n## Step 1: Model Data in Port\n\nDefine two blueprints in Port: `youtube_playlist` for playlists and `youtube_video` for individual videos.\n\n### Playlist Blueprint (`youtube_playlist`)\n\n- **Properties**:\n  - `title` (string): The title of the playlist.\n  - `link` (string): YouTube URL of the playlist.\n  - `description` (string): Description of the playlist.\n  - `publishedAt` (string): Publish date of the playlist.\n  - `channelId` (string): ID of the YouTube channel.\n  - `channelTitle` (string): Title of the YouTube channel.\n  - `thumbnails` (object): Thumbnail images (default, medium, high, standard).\n  - `localized` (object): Localized title and description.\n\n---\n\n   \u003cdetails\u003e\n     \u003csummary\u003eConfiguration mapping for playlist blueprint (click to expand)\u003c/summary\u003e\n\n```json showLineNumbers\n{\n    \"identifier\": \"youtube_playlist\",\n    \"title\": \"YouTube Playlist\",\n    \"description\": \"Blueprint for YouTube Playlist\",\n    \"icon\": \"\",\n    \"schema\": {\n        \"properties\": {\n            \"id\": { \"type\": \"string\" },\n            \"title\": { \"type\": \"string\" },\n            \"link\": { \"type\": \"string\" },\n            \"description\": { \"type\": \"string\" },\n            \"publishedAt\": { \"type\": \"string\" },\n            \"channelId\": { \"type\": \"string\" },\n            \"channelTitle\": { \"type\": \"string\" },\n            \"thumbnails\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"default\": { \"type\": \"string\" },\n                    \"medium\": { \"type\": \"string\" },\n                    \"high\": { \"type\": \"string\" },\n                    \"standard\": { \"type\": \"string\" }\n                }\n            },\n            \"localized\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"title\": {\"type\": \"string\"},\n                    \"description\": {\"type\": \"string\"}\n                }\n            }\n        },\n        \"required\": [\"id\", \"title\", \"description\", \"publishedAt\", \"channelId\", \"channelTitle\"]\n    }\n}\n```\n   \u003c/details\u003e\n\n---\n\n### Video Blueprint (`youtube_video`)\n\n- **Properties**:\n  - `title` (string): Title of the video.\n  - `link` (string): YouTube URL of the video.\n  - `duration` (string): Duration of the video.\n  - `description` (string): Description of the video.\n  - `publishedAt` (string): Publish date of the video.\n  - `position` (integer): Position in the playlist.\n  - `thumbnails` (object): Thumbnail images (default, medium, high, standard, maxres).\n  - `videoOwnerChannelTitle` (string): Title of the owner channel.\n  - `videoOwnerChannelId` (string): ID of the owner channel.\n- **Relationships**:\n  - `playlist`: Links to the `youtube_playlist` entity.\n\n---\n\n   \u003cdetails\u003e\n     \u003csummary\u003eConfiguration mapping for video blueprint (click to expand)\u003c/summary\u003e\n     \n```json showLineNumbers\n{\n    \"identifier\": \"youtube_video\",\n    \"title\": \"YouTube Video\",\n    \"description\": \"Blueprint for YouTube Video\",\n    \"icon\": \"\",\n    \"schema\": {\n        \"properties\": {\n            \"id\": { \"type\": \"string\" },\n            \"title\": { \"type\": \"string\" },\n            \"link\": { \"type\": \"string\" },\n            \"duration\": { \"type\": \"string\" },\n            \"description\": { \"type\": \"string\" },\n            \"publishedAt\": { \"type\": \"string\" },\n            \"position\": { \"type\": \"integer\" },\n            \"thumbnails\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"default\": { \"type\": \"string\" },\n                    \"medium\": { \"type\": \"string\" },\n                    \"high\": { \"type\": \"string\" },\n                    \"standard\": { \"type\": \"string\" },\n                    \"maxres\": { \"type\": \"string\" }\n                }\n            },\n            \"videoOwnerChannelTitle\": { \"type\": \"string\" },\n            \"videoOwnerChannelId\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"title\", \"description\", \"publishedAt\", \"duration\", \"videoLink\"]\n    },\n    \"relations\": {\n        \"playlist\": {\n            \"title\": \"Playlist\",\n            \"many\": false,\n            \"target\": \"youtube_playlist\",\n            \"required\": true\n        }\n    }\n}\n\n```\n   \u003c/details\u003e\n\n---\n\n## Step 2: GitHub Workflow for Data Ingestion\n\nThe following GitHub workflow automates fetching data from YouTube and updating Port with the data.\n\n### GitHub Workflow (`.github/workflows/youtube_port_workflow.yml`)\n\n```yaml showLineNumbers\nname: Update YouTube Playlist and Video Entities in Port\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 0 * * *'  # Runs daily at midnight\n\njobs:\n  update_port_entities:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the code\n        uses: actions/checkout@v2\n\n      - name: Set up Python\n        uses: actions/setup-python@v2\n        with:\n          python-version: '3.9'\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run YouTube Data Fetch and Prepare Port Entities\n        env:\n          YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}\n          CLIENT_ID: ${{ secrets.CLIENT_ID }}\n          CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}\n        run: python fetch_youtube_data.py\n\n      - name: Bulk Create/Update YouTube Playlist and Video Entities in Port\n        uses: port-labs/port-github-action@v1\n        with:\n          clientId: ${{ secrets.CLIENT_ID }}\n          clientSecret: ${{ secrets.CLIENT_SECRET }}\n          baseUrl: https://api.getport.io\n          operation: BULK_UPSERT\n          entities: ${{ toJson(fromJson(file('port_entities.json'))) }}\n```\n\n---\n\n### Fetch YouTube Data\n\nThe `fetch_youtube_data.py` script retrieves YouTube data and prepares it in the required JSON format.\n\n```python\nimport requests\nimport json\nfrom googleapiclient.discovery import build\nimport os\nimport logging\nfrom dotenv import load_dotenv\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO)\nload_dotenv()\n\nYOUTUBE_API_KEY = os.getenv(\"YOUTUBE_API_KEY\")\nPLAYLIST_ID = \"YOUR_PLAYLIST_ID\"\n\ndef fetch_youtube_playlist_data(api_key, playlist_id):\n    youtube = build(\"youtube\", \"v3\", developerKey=api_key)\n    videos = []\n    # Additional logic to fetch video details\n    # ...\n    return videos\n\ndef fetch_youtube_playlist_info(api_key, playlist_id):\n    youtube = build(\"youtube\", \"v3\", developerKey=api_key)\n    request = youtube.playlists().list(\n        part=\"snippet\",\n        id=playlist_id\n    )\n    # Additional logic to fetch playlist details\n    # ...\n    return {\n        \"identifier\": playlist_id,\n        \"blueprint\": \"youtube_playlist\",\n        # Additional playlist props\n        # ...\n    }\n\ndef main():\n    playlist_data = fetch_youtube_playlist_info(YOUTUBE_API_KEY, PLAYLIST_ID)\n    videos_data = fetch_youtube_playlist_data(YOUTUBE_API_KEY, PLAYLIST_ID)\n    # Combine Playlist and Video data for BULK_UPSERT\n    all_data = [playlist_data] + videos_data\n    with open(\"port_entities.json\", \"w\") as f:\n        json.dump(all_data, f, indent=4)\n    logging.info(\"Fetched YouTube data and saved to port_entities.json\")\n\nif __name__ == \"__main__\":\n    main()\n```\n---\n\n   \u003cdetails\u003e\n     \u003csummary\u003eFetch YouTube Data complete implementation (click to expand)\u003c/summary\u003e\n     \n```python showLineNumbers\nimport requests\nimport json\nfrom googleapiclient.discovery import build\nimport os\nimport logging\nfrom dotenv import load_dotenv\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO)\n\nload_dotenv()  # Load environment variables from .env file\n\n# Client credentials\nYOUTUBE_API_KEY = os.getenv(\"YOUTUBE_API_KEY\")\n\n# YouTube playlist details\nPLAYLIST_ID = \"PL5ErBr2d3QJH0kbwTQ7HSuzvBb4zIWzhy\"\n\n\ndef fetch_youtube_playlist_data(api_key, playlist_id):\n    youtube = build(\"youtube\", \"v3\", developerKey=api_key)\n    videos = []\n    next_page_token = None\n\n    while True:\n        playlist_request = youtube.playlistItems().list(\n            part=\"snippet,contentDetails\", playlistId=playlist_id, maxResults=10, pageToken=next_page_token\n        )\n        playlist_response = playlist_request.execute()\n        \n        for item in playlist_response[\"items\"]:\n            video_id = item[\"contentDetails\"][\"videoId\"]\n            \n            # Fetch additional video details including duration\n            video_request = youtube.videos().list(\n                part=\"contentDetails\",\n                id=video_id\n            )\n            video_response = video_request.execute()\n            duration = video_response[\"items\"][0][\"contentDetails\"][\"duration\"]\n\n            title = item[\"snippet\"][\"title\"]\n            description = item[\"snippet\"][\"description\"]\n            publishedAt = item[\"snippet\"][\"publishedAt\"]\n            position = item[\"snippet\"].get(\"position\", None)\n            thumbnails = item[\"snippet\"][\"thumbnails\"]\n            videoOwnerChannelTitle = item[\"snippet\"].get(\"videoOwnerChannelTitle\", \"\")\n            videoOwnerChannelId = item[\"snippet\"].get(\"videoOwnerChannelId\", \"\")\n            video_link = f\"https://www.youtube.com/watch?v={video_id}\"\n\n            videos.append({\n                \"identifier\": video_id,\n                \"blueprint\": \"youtube_video\",\n                \"properties\": {\n                    \"title\": title,\n                    \"duration\": duration,\n                    \"link\": video_link,\n                    \"description\": description,\n                    \"publishedAt\": publishedAt,\n                    \"position\": position,\n                    \"thumbnails\": {\n                        \"default\": thumbnails[\"default\"][\"url\"],\n                        \"medium\": thumbnails[\"medium\"][\"url\"],\n                        \"high\": thumbnails[\"high\"][\"url\"],\n                        \"standard\": thumbnails.get(\"standard\", {}).get(\"url\")\n                    },\n                    \"videoOwnerChannelTitle\": videoOwnerChannelTitle,\n                    \"videoOwnerChannelId\": videoOwnerChannelId\n                },\n                \"relations\": {\n                    \"playlist\": PLAYLIST_ID\n                }\n            })\n\n        next_page_token = playlist_response.get(\"nextPageToken\")\n        if not next_page_token:\n            break\n\n    return videos\n\n\ndef fetch_youtube_playlist_info(api_key, playlist_id):\n    youtube = build(\"youtube\", \"v3\", developerKey=api_key)\n    request = youtube.playlists().list(\n        part=\"snippet\",\n        id=playlist_id\n    )\n    response = request.execute()\n    item = response[\"items\"][0]\n    title = item[\"snippet\"][\"title\"]\n    description = item[\"snippet\"][\"description\"]\n    published_at = item[\"snippet\"][\"publishedAt\"]\n    channel_id = item[\"snippet\"][\"channelId\"]\n    channel_title = item[\"snippet\"][\"channelTitle\"]\n    thumbnails = item[\"snippet\"][\"thumbnails\"]\n    playlist_link = f\"https://www.youtube.com/playlist?list={playlist_id}\"\n    localized_title = item[\"snippet\"][\"localized\"][\"title\"]\n    localized_description = item[\"snippet\"][\"localized\"][\"description\"]\n\n    return {\n        \"identifier\": playlist_id,\n        \"blueprint\": \"youtube_playlist\",\n        \"properties\": {\n            \"title\": title,\n            \"link\": playlist_link,\n            \"description\": description,\n            \"publishedAt\": published_at,\n            \"channelId\": channel_id,\n            \"channelTitle\": channel_title,\n            \"thumbnails\": {\n                \"default\": thumbnails[\"default\"][\"url\"],\n                \"medium\": thumbnails[\"medium\"][\"url\"],\n                \"high\": thumbnails[\"high\"][\"url\"],\n                \"standard\": thumbnails.get(\"standard\", {}).get(\"url\")\n            },\n            \"localized\": {\n                \"title\": localized_title,\n                \"description\": localized_description\n            }\n        }\n    }\n\n\ndef main():\n    playlist_data = fetch_youtube_playlist_info(YOUTUBE_API_KEY, PLAYLIST_ID)\n    videos_data = fetch_youtube_playlist_data(YOUTUBE_API_KEY, PLAYLIST_ID)\n    \n    # Combine Playlist and Video data for BULK_UPSERT\n    all_data = [playlist_data] + videos_data\n    with open(\"port_entities.json\", \"w\") as f:\n        json.dump(all_data, f, indent=4)\n    logging.info(\"Fetched YouTube data and saved to port_entities.json\")\n\n\nif __name__ == \"__main__\":\n    main()\n\n```\n   \u003c/details\u003e\n\n---\n\n### Explanation of Workflow Steps\n\n1. **Check out the code**: Retrieves the repository code.\n2. **Set up Python**: Configures Python 3.9 environment.\n3. **Install dependencies**: Installs required packages from `requirements.txt`.\n4. **Run YouTube Data Fetch**: Runs `fetch_youtube_data.py` to retrieve YouTube data and prepare it for Port ingestion.\n5. **Bulk Create/Update Entities**: Uses Port’s GitHub action to bulk upsert the playlist and video data.\n\n## Step 3: Visualizing Data in Port\n\nOnce data is ingested, you can visualize it in Port by setting up:\n1. A dashboard for tracking playlist-level metrics like the number of videos, publish dates, and durations.\n2. Video-level insights, such as view count, position in playlist, and thumbnail displays.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaulappz%2Fpauloluyege.com","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpaulappz%2Fpauloluyege.com","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaulappz%2Fpauloluyege.com/lists"}